From 39ac9f0ad296d8b99d35ec2e6e0e0ca89ebcc892 Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Mon, 22 Sep 2025 20:12:51 +0200
Subject: [PATCH 001/132] Update README.md
---
README.md | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 7479c61..6664512 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,7 @@
[](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml)
TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel.
-It allows you to manage TRMNL devices, generate screens using native plugins, recipes, or the API, and can optionally act as a proxy for the native cloud service (Core).
-
-If you are looking for a Laravel package designed to streamline the development of both public and private TRMNL plugins, check out [bnussbau/trmnl-laravel](https://github.com/bnussbau/laravel-trmnl).
+It allows you to manage TRMNL devices, generate screens using native plugins, recipes (45+ from the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)), or the API, and can also act as a proxy for the native cloud service (Core).


@@ -17,6 +15,7 @@ If you are looking for a Laravel package designed to streamline the development
* π‘ Device Information β Display battery status, WiFi strength, firmware version, and more.
* π Auto-Join β Automatically detects and adds devices from your local network.
* π₯οΈ Screen Generation β Supports Plugins (even Mashups), Recipes, API, Markup, or updates via Code.
+ * Over 45 compatible open-source recipes are available in the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)
* Supported Devices / Apps: TRMNL, ESP32 with TRMNL firmware, [trmnl-android](https://github.com/usetrmnl/trmnl-android), [trmnl-kindle](https://github.com/usetrmnl/byos_laravel/pull/27), β¦
* π TRMNL API Proxy β Can act as a proxy for the native cloud service (requires TRMNL Developer Edition).
* This enables a hybrid setup β for example, you can update your custom Train Monitor every 5 minutes in the morning, while displaying native TRMNL plugins throughout the day.
@@ -26,11 +25,6 @@ If you are looking for a Laravel package designed to streamline the development

-### π― Target Audience
-
-This project is for developers who are looking for a self-hosted server for devices running the TRMNL firmware.
-It serves as a starter kit, giving you the flexibility to build and extend it however you like.
-
### Support β€οΈ
This repo is maintained voluntarily by [@bnussbau](https://github.com/bnussbau).
@@ -42,6 +36,8 @@ or
[GitHub Sponsors](https://github.com/sponsors/bnussbau/)
+If you are looking for a Laravel package designed to streamline the development of both public and private TRMNL plugins, check out [bnussbau/trmnl-laravel](https://github.com/bnussbau/laravel-trmnl).
+
### Hosting
Run everywhere, where Docker is supported: Raspberry Pi, VPS, NAS, Container Cloud Service (Cloud Run, ...).
From d8f47eb9c2b44970e24a8b8756762f82cd7792ea Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Tue, 23 Sep 2025 11:33:27 +0200
Subject: [PATCH 002/132] Update README.md
---
README.md | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6664512..9d6a620 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,16 @@ It allows you to manage TRMNL devices, generate screens using native plugins, re
* π‘ Device Information β Display battery status, WiFi strength, firmware version, and more.
* π Auto-Join β Automatically detects and adds devices from your local network.
-* π₯οΈ Screen Generation β Supports Plugins (even Mashups), Recipes, API, Markup, or updates via Code.
+* π₯οΈ Screen Generation β Supports Plugins (including Mashups), Recipes, API, Markup, or updates via Code.
* Over 45 compatible open-source recipes are available in the [community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/)
- * Supported Devices / Apps: TRMNL, ESP32 with TRMNL firmware, [trmnl-android](https://github.com/usetrmnl/trmnl-android), [trmnl-kindle](https://github.com/usetrmnl/byos_laravel/pull/27), β¦
+ * Supported Devices
+ * TRMNL OG (1-bit & 2-bit)
+ * SeeedStudio TRMNL 7,5" (OG) DIY Kit
+ * Seeed Studio (XIAO 7.5" ePaper Panel)
+ * reTerminal E1001 Monochrome ePaper Display
+ * Custom ESP32 with TRMNL firmware
+ * Kindle Devices with [trmnl-kindle](https://github.com/usetrmnl/byos_laravel/pull/27)
+ * Android Devices with [trmnl-android](https://github.com/usetrmnl/trmnl-android)
* π TRMNL API Proxy β Can act as a proxy for the native cloud service (requires TRMNL Developer Edition).
* This enables a hybrid setup β for example, you can update your custom Train Monitor every 5 minutes in the morning, while displaying native TRMNL plugins throughout the day.
* π Dark Mode β Switch between light and dark mode.
From 4f251bf37e18dc8efb41a297f6fb072bfef01cd5 Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Tue, 23 Sep 2025 14:24:17 +0200
Subject: [PATCH 003/132] chore: update dependencies
---
composer.lock | 157 +++++++++++++++++++++++-------------------
package-lock.json | 172 +++++++++++++++++++++-------------------------
2 files changed, 164 insertions(+), 165 deletions(-)
diff --git a/composer.lock b/composer.lock
index 86636bc..5a3c004 100644
--- a/composer.lock
+++ b/composer.lock
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.356.20",
+ "version": "3.356.23",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "7514867a6463fb68a60f5e17b4ccc56b4dc7d4f1"
+ "reference": "e9253cf6073f06080a7458af54e18fc474f0c864"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7514867a6463fb68a60f5e17b4ccc56b4dc7d4f1",
- "reference": "7514867a6463fb68a60f5e17b4ccc56b4dc7d4f1",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e9253cf6073f06080a7458af54e18fc474f0c864",
+ "reference": "e9253cf6073f06080a7458af54e18fc474f0c864",
"shasum": ""
},
"require": {
@@ -153,22 +153,22 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.356.20"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.356.23"
},
- "time": "2025-09-17T18:23:32+00:00"
+ "time": "2025-09-22T18:10:31+00:00"
},
{
"name": "bnussbau/laravel-trmnl-blade",
- "version": "2.0.0",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/bnussbau/laravel-trmnl-blade.git",
- "reference": "3b60522bea8ae5dbca94834706247339e1e53582"
+ "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/3b60522bea8ae5dbca94834706247339e1e53582",
- "reference": "3b60522bea8ae5dbca94834706247339e1e53582",
+ "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/59343cfa9c41c7c7f9285b366584cde92bf1294e",
+ "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e",
"shasum": ""
},
"require": {
@@ -223,7 +223,7 @@
],
"support": {
"issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues",
- "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.0.0"
+ "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.0.1"
},
"funding": [
{
@@ -239,7 +239,7 @@
"type": "github"
}
],
- "time": "2025-09-14T07:54:31+00:00"
+ "time": "2025-09-22T12:12:00+00:00"
},
{
"name": "bnussbau/trmnl-pipeline-php",
@@ -1618,16 +1618,16 @@
},
{
"name": "laravel/framework",
- "version": "v12.30.0",
+ "version": "v12.30.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "943603722fe95b69f216bdcda7d060c9a55f18fd"
+ "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/943603722fe95b69f216bdcda7d060c9a55f18fd",
- "reference": "943603722fe95b69f216bdcda7d060c9a55f18fd",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/7f61e8679f9142f282a0184ac7ef9e3834bfd023",
+ "reference": "7f61e8679f9142f282a0184ac7ef9e3834bfd023",
"shasum": ""
},
"require": {
@@ -1834,7 +1834,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-09-18T15:10:15+00:00"
+ "time": "2025-09-18T21:07:07+00:00"
},
{
"name": "laravel/prompts",
@@ -3705,24 +3705,26 @@
},
{
"name": "paragonie/constant_time_encoding",
- "version": "v3.0.0",
+ "version": "v3.1.1",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
- "reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
+ "reference": "5e9b582660b997de205a84c02a3aac7c060900c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
- "reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/5e9b582660b997de205a84c02a3aac7c060900c9",
+ "reference": "5e9b582660b997de205a84c02a3aac7c060900c9",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
- "phpunit/phpunit": "^9",
- "vimeo/psalm": "^4|^5"
+ "infection/infection": "^0",
+ "nikic/php-fuzzer": "^0",
+ "phpunit/phpunit": "^9|^10|^11",
+ "vimeo/psalm": "^4|^5|^6"
},
"type": "library",
"autoload": {
@@ -3768,7 +3770,7 @@
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
- "time": "2024-05-08T12:36:18+00:00"
+ "time": "2025-09-22T21:00:33+00:00"
},
{
"name": "paragonie/random_compat",
@@ -3822,16 +3824,16 @@
},
{
"name": "phiki/phiki",
- "version": "v2.0.2",
+ "version": "v2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phikiphp/phiki.git",
- "reference": "6d735108238c03daaaef571448d8dee8187cab5e"
+ "reference": "160785c50c01077780ab217e5808f00ab8f05a13"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phikiphp/phiki/zipball/6d735108238c03daaaef571448d8dee8187cab5e",
- "reference": "6d735108238c03daaaef571448d8dee8187cab5e",
+ "url": "https://api.github.com/repos/phikiphp/phiki/zipball/160785c50c01077780ab217e5808f00ab8f05a13",
+ "reference": "160785c50c01077780ab217e5808f00ab8f05a13",
"shasum": ""
},
"require": {
@@ -3877,7 +3879,7 @@
"description": "Syntax highlighting using TextMate grammars in PHP.",
"support": {
"issues": "https://github.com/phikiphp/phiki/issues",
- "source": "https://github.com/phikiphp/phiki/tree/v2.0.2"
+ "source": "https://github.com/phikiphp/phiki/tree/v2.0.4"
},
"funding": [
{
@@ -3889,7 +3891,7 @@
"type": "other"
}
],
- "time": "2025-09-17T18:32:40+00:00"
+ "time": "2025-09-20T17:21:02+00:00"
},
{
"name": "phpoption/phpoption",
@@ -4490,16 +4492,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.10",
+ "version": "v0.12.12",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22"
+ "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22",
- "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/cd23863404a40ccfaf733e3af4db2b459837f7e7",
+ "reference": "cd23863404a40ccfaf733e3af4db2b459837f7e7",
"shasum": ""
},
"require": {
@@ -4562,9 +4564,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.12.10"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.12"
},
- "time": "2025-08-04T12:39:37+00:00"
+ "time": "2025-09-20T13:46:31+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -8439,16 +8441,16 @@
},
{
"name": "larastan/larastan",
- "version": "v3.7.1",
+ "version": "v3.7.2",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
- "reference": "2e653fd19585a825e283b42f38378b21ae481cc7"
+ "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/larastan/larastan/zipball/2e653fd19585a825e283b42f38378b21ae481cc7",
- "reference": "2e653fd19585a825e283b42f38378b21ae481cc7",
+ "url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae",
+ "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae",
"shasum": ""
},
"require": {
@@ -8462,7 +8464,7 @@
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"php": "^8.2",
- "phpstan/phpstan": "^2.1.23"
+ "phpstan/phpstan": "^2.1.28"
},
"require-dev": {
"doctrine/coding-standard": "^13",
@@ -8516,7 +8518,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
- "source": "https://github.com/larastan/larastan/tree/v3.7.1"
+ "source": "https://github.com/larastan/larastan/tree/v3.7.2"
},
"funding": [
{
@@ -8524,20 +8526,20 @@
"type": "github"
}
],
- "time": "2025-09-10T19:42:11+00:00"
+ "time": "2025-09-19T09:03:05+00:00"
},
{
"name": "laravel/boost",
- "version": "v1.2.0",
+ "version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
- "reference": "85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24"
+ "reference": "84cd7630849df6f54d8cccb047fba5d83442ef93"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/boost/zipball/85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24",
- "reference": "85f7de54a6b60f684fc9f7f6df5ad94f4f7d0d24",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/84cd7630849df6f54d8cccb047fba5d83442ef93",
+ "reference": "84cd7630849df6f54d8cccb047fba5d83442ef93",
"shasum": ""
},
"require": {
@@ -8548,7 +8550,7 @@
"illuminate/support": "^10.49.0|^11.45.3|^12.28.1",
"laravel/mcp": "^0.2.0",
"laravel/prompts": "0.1.25|^0.3.6",
- "laravel/roster": "^0.2.6",
+ "laravel/roster": "^0.2.8",
"php": "^8.1"
},
"require-dev": {
@@ -8556,7 +8558,8 @@
"mockery/mockery": "^1.6.12",
"orchestra/testbench": "^8.36.0|^9.15.0|^10.6",
"pestphp/pest": "^2.36.0|^3.8.4",
- "phpstan/phpstan": "^2.1.27"
+ "phpstan/phpstan": "^2.1.27",
+ "rector/rector": "^2.1"
},
"type": "library",
"extra": {
@@ -8589,7 +8592,7 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
- "time": "2025-09-18T13:05:07+00:00"
+ "time": "2025-09-23T07:31:42+00:00"
},
{
"name": "laravel/mcp",
@@ -8745,16 +8748,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.25.0",
+ "version": "v1.25.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96"
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96",
- "reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
"shasum": ""
},
"require": {
@@ -8807,20 +8810,20 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2025-09-17T01:36:44+00:00"
+ "time": "2025-09-19T02:57:12+00:00"
},
{
"name": "laravel/roster",
- "version": "v0.2.7",
+ "version": "v0.2.8",
"source": {
"type": "git",
"url": "https://github.com/laravel/roster.git",
- "reference": "9de07bfb52cfe4e5a1fec10b8a446d6add8376cd"
+ "reference": "832a6db43743bf08a58691da207f977ec8dc43aa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/roster/zipball/9de07bfb52cfe4e5a1fec10b8a446d6add8376cd",
- "reference": "9de07bfb52cfe4e5a1fec10b8a446d6add8376cd",
+ "url": "https://api.github.com/repos/laravel/roster/zipball/832a6db43743bf08a58691da207f977ec8dc43aa",
+ "reference": "832a6db43743bf08a58691da207f977ec8dc43aa",
"shasum": ""
},
"require": {
@@ -8868,7 +8871,7 @@
"issues": "https://github.com/laravel/roster/issues",
"source": "https://github.com/laravel/roster"
},
- "time": "2025-09-18T13:53:41+00:00"
+ "time": "2025-09-22T13:28:47+00:00"
},
{
"name": "laravel/sail",
@@ -10048,16 +10051,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.27",
+ "version": "2.1.28",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "25da374959afa391992792691093550b3098ef1e"
+ "reference": "578fa296a166605d97b94091f724f1257185d278"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/25da374959afa391992792691093550b3098ef1e",
- "reference": "25da374959afa391992792691093550b3098ef1e",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278",
+ "reference": "578fa296a166605d97b94091f724f1257185d278",
"shasum": ""
},
"require": {
@@ -10102,7 +10105,7 @@
"type": "github"
}
],
- "time": "2025-09-17T09:55:13+00:00"
+ "time": "2025-09-19T08:58:49+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -10907,16 +10910,16 @@
},
{
"name": "sebastian/exporter",
- "version": "7.0.0",
+ "version": "7.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "76432aafc58d50691a00d86d0632f1217a47b688"
+ "reference": "b759164a8e02263784b662889cc6cbb686077af6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688",
- "reference": "76432aafc58d50691a00d86d0632f1217a47b688",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/b759164a8e02263784b662889cc6cbb686077af6",
+ "reference": "b759164a8e02263784b662889cc6cbb686077af6",
"shasum": ""
},
"require": {
@@ -10973,15 +10976,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2025-02-07T04:56:42+00:00"
+ "time": "2025-09-22T05:39:29+00:00"
},
{
"name": "sebastian/global-state",
diff --git a/package-lock.json b/package-lock.json
index b57b9bd..d434d4b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -507,9 +507,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.30",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
- "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -798,24 +798,24 @@
]
},
"node_modules/@tailwindcss/node": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz",
- "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
+ "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.5.1",
"lightningcss": "1.30.1",
- "magic-string": "^0.30.17",
+ "magic-string": "^0.30.18",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.1.12"
+ "tailwindcss": "4.1.13"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz",
- "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
+ "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -826,24 +826,24 @@
"node": ">= 10"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.12",
- "@tailwindcss/oxide-darwin-arm64": "4.1.12",
- "@tailwindcss/oxide-darwin-x64": "4.1.12",
- "@tailwindcss/oxide-freebsd-x64": "4.1.12",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.12",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.12",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.12",
- "@tailwindcss/oxide-wasm32-wasi": "4.1.12",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.12"
+ "@tailwindcss/oxide-android-arm64": "4.1.13",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.13",
+ "@tailwindcss/oxide-darwin-x64": "4.1.13",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.13",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.13",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.13",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz",
- "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
+ "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
"cpu": [
"arm64"
],
@@ -857,9 +857,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz",
- "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
+ "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
"cpu": [
"arm64"
],
@@ -873,9 +873,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz",
- "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
+ "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
"cpu": [
"x64"
],
@@ -889,9 +889,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz",
- "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
+ "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
"cpu": [
"x64"
],
@@ -905,9 +905,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz",
- "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
+ "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
"cpu": [
"arm"
],
@@ -921,9 +921,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz",
- "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
+ "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
"cpu": [
"arm64"
],
@@ -937,9 +937,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz",
- "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
+ "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
"cpu": [
"arm64"
],
@@ -953,9 +953,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz",
- "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
+ "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
"cpu": [
"x64"
],
@@ -969,9 +969,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz",
- "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
+ "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
"cpu": [
"x64"
],
@@ -985,9 +985,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz",
- "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
+ "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -1014,9 +1014,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz",
- "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
+ "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
"cpu": [
"arm64"
],
@@ -1030,9 +1030,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz",
- "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
+ "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
"cpu": [
"x64"
],
@@ -1046,14 +1046,14 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz",
- "integrity": "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz",
+ "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==",
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "4.1.12",
- "@tailwindcss/oxide": "4.1.12",
- "tailwindcss": "4.1.12"
+ "@tailwindcss/node": "4.1.13",
+ "@tailwindcss/oxide": "4.1.13",
+ "tailwindcss": "4.1.13"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
@@ -2414,9 +2414,9 @@
}
},
"node_modules/magic-string": {
- "version": "0.30.18",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
- "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
+ "version": "0.30.19",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
+ "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -2462,9 +2462,9 @@
}
},
"node_modules/minizlib": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
- "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
@@ -2479,21 +2479,6 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
- "node_modules/mkdirp": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
- "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
- "license": "MIT",
- "bin": {
- "mkdirp": "dist/cjs/src/bin.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2968,9 +2953,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz",
- "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==",
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
+ "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
"license": "MIT"
},
"node_modules/tapable": {
@@ -2987,16 +2972,15 @@
}
},
"node_modules/tar": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
- "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.4.tgz",
+ "integrity": "sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
- "minizlib": "^3.0.1",
- "mkdirp": "^3.0.1",
+ "minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
From 42b515e3228c2ad30a83b44ce3bdeafe3be0a311 Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Tue, 23 Sep 2025 23:56:11 +0200
Subject: [PATCH 004/132] test: improve coverage
---
app/Console/Commands/MashupCreateCommand.php | 52 +--
app/Console/Commands/OidcTestCommand.php | 7 +-
app/Jobs/FirmwareDownloadJob.php | 13 +-
app/Services/OidcProvider.php | 4 +-
.../ExampleRecipesSeederCommandTest.php | 40 ++
.../Console/FirmwareCheckCommandTest.php | 29 ++
.../Console/FirmwareUpdateCommandTest.php | 86 +++++
.../Console/MashupCreateCommandTest.php | 154 ++++++++
tests/Feature/Console/OidcTestCommandTest.php | 188 ++++++++++
.../Feature/Jobs/FetchDeviceModelsJobTest.php | 344 ++++++++++++++++++
.../Feature/Jobs/FirmwareDownloadJobTest.php | 119 ++++++
.../Jobs/NotifyDeviceBatteryLowJobTest.php | 140 +++++++
.../Livewire/Actions/DeviceAutoJoinTest.php | 115 ++++++
.../Unit/Liquid/Filters/LocalizationTest.php | 75 ++++
tests/Unit/Liquid/Filters/NumbersTest.php | 95 ++++-
.../Unit/Liquid/Filters/StringMarkupTest.php | 80 ++++
tests/Unit/Models/DeviceModelTest.php | 119 ++++++
tests/Unit/Notifications/BatteryLowTest.php | 76 ++++
.../Unit/Notifications/WebhookChannelTest.php | 135 +++++++
.../Unit/Notifications/WebhookMessageTest.php | 92 +++++
tests/Unit/Services/OidcProviderTest.php | 281 ++++++++++++++
21 files changed, 2212 insertions(+), 32 deletions(-)
create mode 100644 tests/Feature/Console/ExampleRecipesSeederCommandTest.php
create mode 100644 tests/Feature/Console/FirmwareCheckCommandTest.php
create mode 100644 tests/Feature/Console/FirmwareUpdateCommandTest.php
create mode 100644 tests/Feature/Console/MashupCreateCommandTest.php
create mode 100644 tests/Feature/Console/OidcTestCommandTest.php
create mode 100644 tests/Feature/Jobs/FetchDeviceModelsJobTest.php
create mode 100644 tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
create mode 100644 tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
create mode 100644 tests/Unit/Models/DeviceModelTest.php
create mode 100644 tests/Unit/Notifications/BatteryLowTest.php
create mode 100644 tests/Unit/Notifications/WebhookChannelTest.php
create mode 100644 tests/Unit/Notifications/WebhookMessageTest.php
create mode 100644 tests/Unit/Services/OidcProviderTest.php
diff --git a/app/Console/Commands/MashupCreateCommand.php b/app/Console/Commands/MashupCreateCommand.php
index d6f1378..7020235 100644
--- a/app/Console/Commands/MashupCreateCommand.php
+++ b/app/Console/Commands/MashupCreateCommand.php
@@ -9,9 +9,6 @@ use App\Models\Plugin;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
-use function Laravel\Prompts\select;
-use function Laravel\Prompts\text;
-
class MashupCreateCommand extends Command
{
/**
@@ -88,9 +85,9 @@ class MashupCreateCommand extends Command
return null;
}
- $deviceId = select(
- label: 'Select a device',
- options: $devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray()
+ $deviceId = $this->choice(
+ 'Select a device',
+ $devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray()
);
return $devices->firstWhere('id', $deviceId);
@@ -106,9 +103,9 @@ class MashupCreateCommand extends Command
return null;
}
- $playlistId = select(
- label: 'Select a playlist',
- options: $playlists->mapWithKeys(fn (Playlist $playlist) => [$playlist->id => $playlist->name])->toArray()
+ $playlistId = $this->choice(
+ 'Select a playlist',
+ $playlists->mapWithKeys(fn (Playlist $playlist) => [$playlist->id => $playlist->name])->toArray()
);
return $playlists->firstWhere('id', $playlistId);
@@ -116,24 +113,29 @@ class MashupCreateCommand extends Command
protected function selectLayout(): ?string
{
- return select(
- label: 'Select a layout',
- options: PlaylistItem::getAvailableLayouts()
+ return $this->choice(
+ 'Select a layout',
+ PlaylistItem::getAvailableLayouts()
);
}
protected function getMashupName(): ?string
{
- return text(
- label: 'Enter a name for this mashup',
- required: true,
- default: 'Mashup',
- validate: fn (string $value) => match (true) {
- mb_strlen($value) < 1 => 'The name must be at least 2 characters.',
- mb_strlen($value) > 50 => 'The name must not exceed 50 characters.',
- default => null,
- }
- );
+ $name = $this->ask('Enter a name for this mashup', 'Mashup');
+
+ if (mb_strlen($name) < 2) {
+ $this->error('The name must be at least 2 characters.');
+
+ return null;
+ }
+
+ if (mb_strlen($name) > 50) {
+ $this->error('The name must not exceed 50 characters.');
+
+ return null;
+ }
+
+ return $name;
}
protected function selectPlugins(string $layout): Collection
@@ -159,9 +161,9 @@ class MashupCreateCommand extends Command
default => ($i + 1).'th'
};
- $pluginId = select(
- label: "Select the $position plugin",
- options: $availablePlugins
+ $pluginId = $this->choice(
+ "Select the $position plugin",
+ $availablePlugins
);
$selectedPlugins->push($plugins->firstWhere('id', $pluginId));
diff --git a/app/Console/Commands/OidcTestCommand.php b/app/Console/Commands/OidcTestCommand.php
index c04f263..73321ce 100644
--- a/app/Console/Commands/OidcTestCommand.php
+++ b/app/Console/Commands/OidcTestCommand.php
@@ -40,13 +40,18 @@ class OidcTestCommand extends Command
$clientId = config('services.oidc.client_id');
$clientSecret = config('services.oidc.client_secret');
$redirect = config('services.oidc.redirect');
+ if (! $redirect) {
+ $redirect = config('app.url', 'http://localhost').'/auth/oidc/callback';
+ }
$scopes = config('services.oidc.scopes', []);
+ $defaultScopes = ['openid', 'profile', 'email'];
+ $effectiveScopes = empty($scopes) ? $defaultScopes : $scopes;
$this->line('OIDC Endpoint: '.($endpoint ? "β
{$endpoint}" : 'β Not set'));
$this->line('Client ID: '.($clientId ? "β
{$clientId}" : 'β Not set'));
$this->line('Client Secret: '.($clientSecret ? 'β
Set' : 'β Not set'));
$this->line('Redirect URL: '.($redirect ? "β
{$redirect}" : 'β Not set'));
- $this->line('Scopes: '.(empty($scopes) ? 'β Not set' : 'β
'.implode(', ', $scopes)));
+ $this->line('Scopes: β
'.implode(', ', $effectiveScopes));
$this->newLine();
diff --git a/app/Jobs/FirmwareDownloadJob.php b/app/Jobs/FirmwareDownloadJob.php
index 6b4fc36..13352c3 100644
--- a/app/Jobs/FirmwareDownloadJob.php
+++ b/app/Jobs/FirmwareDownloadJob.php
@@ -33,16 +33,25 @@ class FirmwareDownloadJob implements ShouldQueue
try {
$filename = "FW{$this->firmware->version_tag}.bin";
- Http::sink(storage_path("app/public/firmwares/$filename"))
- ->get($this->firmware->url);
+ $response = Http::get($this->firmware->url);
+ if (! $response->successful()) {
+ throw new Exception('HTTP request failed with status: '.$response->status());
+ }
+
+ // Save the response content to file
+ Storage::disk('public')->put("firmwares/$filename", $response->body());
+
+ // Only update storage location if download was successful
$this->firmware->update([
'storage_location' => "firmwares/$filename",
]);
} catch (ConnectionException $e) {
Log::error('Firmware download failed: '.$e->getMessage());
+ // Don't update storage_location on failure
} catch (Exception $e) {
Log::error('An unexpected error occurred: '.$e->getMessage());
+ // Don't update storage_location on failure
}
}
}
diff --git a/app/Services/OidcProvider.php b/app/Services/OidcProvider.php
index e6cda63..74143f1 100644
--- a/app/Services/OidcProvider.php
+++ b/app/Services/OidcProvider.php
@@ -60,7 +60,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
{
try {
$url = $this->baseUrl.'/.well-known/openid-configuration';
- $client = new Client();
+ $client = app(Client::class);
$response = $client->get($url);
$this->oidcConfig = json_decode($response->getBody()->getContents(), true);
@@ -122,7 +122,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
/**
* Map the raw user array to a Socialite User instance.
*/
- protected function mapUserToObject(array $user)
+ public function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['sub'],
diff --git a/tests/Feature/Console/ExampleRecipesSeederCommandTest.php b/tests/Feature/Console/ExampleRecipesSeederCommandTest.php
new file mode 100644
index 0000000..4b98180
--- /dev/null
+++ b/tests/Feature/Console/ExampleRecipesSeederCommandTest.php
@@ -0,0 +1,40 @@
+shouldReceive('run')
+ ->once()
+ ->with('123');
+
+ $this->app->instance(ExampleRecipesSeeder::class, $seeder);
+
+ $this->artisan('recipes:seed', ['user_id' => '123'])
+ ->assertExitCode(0);
+});
+
+test('example recipes seeder command has correct signature', function () {
+ $command = $this->app->make(App\Console\Commands\ExampleRecipesSeederCommand::class);
+
+ expect($command->getName())->toBe('recipes:seed');
+ expect($command->getDescription())->toBe('Seed example recipes');
+});
+
+test('example recipes seeder command prompts for missing input', function () {
+ $seeder = Mockery::mock(ExampleRecipesSeeder::class);
+ $seeder->shouldReceive('run')
+ ->once()
+ ->with('456');
+
+ $this->app->instance(ExampleRecipesSeeder::class, $seeder);
+
+ $this->artisan('recipes:seed')
+ ->expectsQuestion('What is the user_id?', '456')
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/Console/FirmwareCheckCommandTest.php b/tests/Feature/Console/FirmwareCheckCommandTest.php
new file mode 100644
index 0000000..19098ea
--- /dev/null
+++ b/tests/Feature/Console/FirmwareCheckCommandTest.php
@@ -0,0 +1,29 @@
+app->make(App\Console\Commands\FirmwareCheckCommand::class);
+
+ expect($command->getName())->toBe('trmnl:firmware:check');
+ expect($command->getDescription())->toBe('Checks for the latest firmware and downloads it if flag --download is passed.');
+});
+
+test('firmware check command runs without errors', function () {
+ $this->artisan('trmnl:firmware:check')
+ ->assertExitCode(0);
+});
+
+test('firmware check command runs with download flag', function () {
+ $this->artisan('trmnl:firmware:check', ['--download' => true])
+ ->assertExitCode(0);
+});
+
+test('firmware check command can run successfully', function () {
+ $this->artisan('trmnl:firmware:check')
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/Console/FirmwareUpdateCommandTest.php b/tests/Feature/Console/FirmwareUpdateCommandTest.php
new file mode 100644
index 0000000..ee250b9
--- /dev/null
+++ b/tests/Feature/Console/FirmwareUpdateCommandTest.php
@@ -0,0 +1,86 @@
+artisan('trmnl:firmware:update --help')
+ ->assertExitCode(0);
+});
+
+test('firmware update command can be called', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
+
+ $this->artisan('trmnl:firmware:update')
+ ->expectsQuestion('Check for new firmware?', 'no')
+ ->expectsQuestion('Update to which version?', $firmware->id)
+ ->expectsQuestion('Which devices should be updated?', ["_$device->id"])
+ ->assertExitCode(0);
+
+ $device->refresh();
+ expect($device->update_firmware_id)->toBe($firmware->id);
+});
+
+test('firmware update command updates all devices when all is selected', function () {
+ $user = User::factory()->create();
+ $device1 = Device::factory()->create(['user_id' => $user->id]);
+ $device2 = Device::factory()->create(['user_id' => $user->id]);
+ $firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
+
+ $this->artisan('trmnl:firmware:update')
+ ->expectsQuestion('Check for new firmware?', 'no')
+ ->expectsQuestion('Update to which version?', $firmware->id)
+ ->expectsQuestion('Which devices should be updated?', ['all'])
+ ->assertExitCode(0);
+
+ $device1->refresh();
+ $device2->refresh();
+ expect($device1->update_firmware_id)->toBe($firmware->id);
+ expect($device2->update_firmware_id)->toBe($firmware->id);
+});
+
+test('firmware update command aborts when no devices selected', function () {
+ $firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
+
+ $this->artisan('trmnl:firmware:update')
+ ->expectsQuestion('Check for new firmware?', 'no')
+ ->expectsQuestion('Update to which version?', $firmware->id)
+ ->expectsQuestion('Which devices should be updated?', [])
+ ->expectsOutput('No devices selected. Aborting.')
+ ->assertExitCode(0);
+});
+
+test('firmware update command calls firmware check when check is selected', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
+
+ $this->artisan('trmnl:firmware:update')
+ ->expectsQuestion('Check for new firmware?', 'check')
+ ->expectsQuestion('Update to which version?', $firmware->id)
+ ->expectsQuestion('Which devices should be updated?', ["_$device->id"])
+ ->assertExitCode(0);
+
+ $device->refresh();
+ expect($device->update_firmware_id)->toBe($firmware->id);
+});
+
+test('firmware update command calls firmware check with download when download is selected', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
+
+ $this->artisan('trmnl:firmware:update')
+ ->expectsQuestion('Check for new firmware?', 'download')
+ ->expectsQuestion('Update to which version?', $firmware->id)
+ ->expectsQuestion('Which devices should be updated?', ["_$device->id"])
+ ->assertExitCode(0);
+
+ $device->refresh();
+ expect($device->update_firmware_id)->toBe($firmware->id);
+});
diff --git a/tests/Feature/Console/MashupCreateCommandTest.php b/tests/Feature/Console/MashupCreateCommandTest.php
new file mode 100644
index 0000000..e61c34c
--- /dev/null
+++ b/tests/Feature/Console/MashupCreateCommandTest.php
@@ -0,0 +1,154 @@
+artisan('mashup:create --help')
+ ->assertExitCode(0);
+});
+
+test('mashup create command creates mashup successfully', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $playlist = Playlist::factory()->create(['device_id' => $device->id]);
+ $plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
+ $plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsQuestion('Select a playlist', $playlist->id)
+ ->expectsQuestion('Select a layout', '1Lx1R')
+ ->expectsQuestion('Enter a name for this mashup', 'Test Mashup')
+ ->expectsQuestion('Select the first plugin', $plugin1->id)
+ ->expectsQuestion('Select the second plugin', $plugin2->id)
+ ->expectsOutput('Mashup created successfully!')
+ ->assertExitCode(0);
+
+ $playlistItem = PlaylistItem::where('playlist_id', $playlist->id)
+ ->whereJsonContains('mashup->mashup_name', 'Test Mashup')
+ ->first();
+
+ expect($playlistItem)->not->toBeNull();
+ expect($playlistItem->isMashup())->toBeTrue();
+ expect($playlistItem->getMashupLayoutType())->toBe('1Lx1R');
+ expect($playlistItem->getMashupPluginIds())->toContain($plugin1->id, $plugin2->id);
+});
+
+test('mashup create command exits when no devices found', function () {
+ $this->artisan('mashup:create')
+ ->expectsOutput('No devices found. Please create a device first.')
+ ->assertExitCode(1);
+});
+
+test('mashup create command exits when no playlists found for device', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsOutput('No playlists found for this device. Please create a playlist first.')
+ ->assertExitCode(1);
+});
+
+test('mashup create command exits when no plugins found', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $playlist = Playlist::factory()->create(['device_id' => $device->id]);
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsQuestion('Select a playlist', $playlist->id)
+ ->expectsQuestion('Select a layout', '1Lx1R')
+ ->expectsQuestion('Enter a name for this mashup', 'Test Mashup')
+ ->expectsOutput('No plugins found. Please create some plugins first.')
+ ->assertExitCode(1);
+});
+
+test('mashup create command validates mashup name length', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $playlist = Playlist::factory()->create(['device_id' => $device->id]);
+ $plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
+ $plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsQuestion('Select a playlist', $playlist->id)
+ ->expectsQuestion('Select a layout', '1Lx1R')
+ ->expectsQuestion('Enter a name for this mashup', 'A') // Too short
+ ->expectsOutput('The name must be at least 2 characters.')
+ ->assertExitCode(1);
+});
+
+test('mashup create command validates mashup name maximum length', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $playlist = Playlist::factory()->create(['device_id' => $device->id]);
+ $plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
+ $plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
+
+ $longName = str_repeat('A', 51); // Too long
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsQuestion('Select a playlist', $playlist->id)
+ ->expectsQuestion('Select a layout', '1Lx1R')
+ ->expectsQuestion('Enter a name for this mashup', $longName)
+ ->expectsOutput('The name must not exceed 50 characters.')
+ ->assertExitCode(1);
+});
+
+test('mashup create command uses default name when provided', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $playlist = Playlist::factory()->create(['device_id' => $device->id]);
+ $plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
+ $plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsQuestion('Select a playlist', $playlist->id)
+ ->expectsQuestion('Select a layout', '1Lx1R')
+ ->expectsQuestion('Enter a name for this mashup', 'Mashup') // Default value
+ ->expectsQuestion('Select the first plugin', $plugin1->id)
+ ->expectsQuestion('Select the second plugin', $plugin2->id)
+ ->expectsOutput('Mashup created successfully!')
+ ->assertExitCode(0);
+
+ $playlistItem = PlaylistItem::where('playlist_id', $playlist->id)
+ ->whereJsonContains('mashup->mashup_name', 'Mashup')
+ ->first();
+
+ expect($playlistItem)->not->toBeNull();
+});
+
+test('mashup create command handles 1x1 layout with single plugin', function () {
+ $user = User::factory()->create();
+ $device = Device::factory()->create(['user_id' => $user->id]);
+ $playlist = Playlist::factory()->create(['device_id' => $device->id]);
+ $plugin = Plugin::factory()->create(['user_id' => $user->id]);
+
+ $this->artisan('mashup:create')
+ ->expectsQuestion('Select a device', $device->id)
+ ->expectsQuestion('Select a playlist', $playlist->id)
+ ->expectsQuestion('Select a layout', '1x1')
+ ->expectsQuestion('Enter a name for this mashup', 'Single Plugin Mashup')
+ ->expectsQuestion('Select the first plugin', $plugin->id)
+ ->expectsOutput('Mashup created successfully!')
+ ->assertExitCode(0);
+
+ $playlistItem = PlaylistItem::where('playlist_id', $playlist->id)
+ ->whereJsonContains('mashup->mashup_name', 'Single Plugin Mashup')
+ ->first();
+
+ expect($playlistItem)->not->toBeNull();
+ expect($playlistItem->getMashupLayoutType())->toBe('1x1');
+ expect($playlistItem->getMashupPluginIds())->toHaveCount(1);
+ expect($playlistItem->getMashupPluginIds())->toContain($plugin->id);
+});
diff --git a/tests/Feature/Console/OidcTestCommandTest.php b/tests/Feature/Console/OidcTestCommandTest.php
new file mode 100644
index 0000000..e7456b0
--- /dev/null
+++ b/tests/Feature/Console/OidcTestCommandTest.php
@@ -0,0 +1,188 @@
+artisan('oidc:test --help')
+ ->assertExitCode(0);
+});
+
+test('oidc test command runs successfully with disabled oidc', function () {
+ config(['services.oidc.enabled' => false]);
+
+ $this->artisan('oidc:test')
+ ->expectsOutput('Testing OIDC Configuration...')
+ ->expectsOutput('OIDC Enabled: β No')
+ ->expectsOutput('OIDC Endpoint: β Not set')
+ ->expectsOutput('Client ID: β Not set')
+ ->expectsOutput('Client Secret: β Not set')
+ ->expectsOutput('Redirect URL: β
http://localhost/auth/oidc/callback')
+ ->expectsOutput('Scopes: β
openid, profile, email')
+ ->expectsOutput('OIDC Driver: β
Registered (configuration test skipped due to missing values)')
+ ->expectsOutput('β οΈ OIDC driver is registered but missing required configuration.')
+ ->expectsOutput('Please set the following environment variables:')
+ ->expectsOutput(' - OIDC_ENABLED=true')
+ ->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com (base URL)')
+ ->expectsOutput(' OR')
+ ->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com/.well-known/openid-configuration (full URL)')
+ ->expectsOutput(' - OIDC_CLIENT_ID=your-client-id')
+ ->expectsOutput(' - OIDC_CLIENT_SECRET=your-client-secret')
+ ->assertExitCode(0);
+});
+
+test('oidc test command runs successfully with enabled oidc but missing config', function () {
+ config([
+ 'services.oidc.enabled' => true,
+ 'services.oidc.endpoint' => null,
+ 'services.oidc.client_id' => null,
+ 'services.oidc.client_secret' => null,
+ 'services.oidc.redirect' => null,
+ 'services.oidc.scopes' => [],
+ ]);
+
+ $this->artisan('oidc:test')
+ ->expectsOutput('Testing OIDC Configuration...')
+ ->expectsOutput('OIDC Enabled: β
Yes')
+ ->expectsOutput('OIDC Endpoint: β Not set')
+ ->expectsOutput('Client ID: β Not set')
+ ->expectsOutput('Client Secret: β Not set')
+ ->expectsOutput('Redirect URL: β
http://localhost/auth/oidc/callback')
+ ->expectsOutput('Scopes: β
openid, profile, email')
+ ->expectsOutput('OIDC Driver: β
Registered (configuration test skipped due to missing values)')
+ ->expectsOutput('β οΈ OIDC driver is registered but missing required configuration.')
+ ->expectsOutput('Please set the following environment variables:')
+ ->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com (base URL)')
+ ->expectsOutput(' OR')
+ ->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com/.well-known/openid-configuration (full URL)')
+ ->expectsOutput(' - OIDC_CLIENT_ID=your-client-id')
+ ->expectsOutput(' - OIDC_CLIENT_SECRET=your-client-secret')
+ ->assertExitCode(0);
+});
+
+test('oidc test command runs successfully with partial config', function () {
+ config([
+ 'services.oidc.enabled' => true,
+ 'services.oidc.endpoint' => 'https://example.com',
+ 'services.oidc.client_id' => 'test-client-id',
+ 'services.oidc.client_secret' => null,
+ 'services.oidc.redirect' => 'https://example.com/callback',
+ 'services.oidc.scopes' => ['openid', 'profile'],
+ ]);
+
+ $this->artisan('oidc:test')
+ ->expectsOutput('Testing OIDC Configuration...')
+ ->expectsOutput('OIDC Enabled: β
Yes')
+ ->expectsOutput('OIDC Endpoint: β
https://example.com')
+ ->expectsOutput('Client ID: β
test-client-id')
+ ->expectsOutput('Client Secret: β Not set')
+ ->expectsOutput('Redirect URL: β
https://example.com/callback')
+ ->expectsOutput('Scopes: β
openid, profile')
+ ->expectsOutput('OIDC Driver: β
Registered (configuration test skipped due to missing values)')
+ ->expectsOutput('β οΈ OIDC driver is registered but missing required configuration.')
+ ->expectsOutput('Please set the following environment variables:')
+ ->expectsOutput(' - OIDC_CLIENT_SECRET=your-client-secret')
+ ->assertExitCode(0);
+});
+
+test('oidc test command runs successfully with full config but disabled', function () {
+ // Mock the HTTP client to return fake OIDC configuration
+ mock(GuzzleHttp\Client::class, function ($mock) {
+ $mock->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ])));
+ });
+
+ config([
+ 'services.oidc.enabled' => false,
+ 'services.oidc.endpoint' => 'https://example.com',
+ 'services.oidc.client_id' => 'test-client-id',
+ 'services.oidc.client_secret' => 'test-client-secret',
+ 'services.oidc.redirect' => 'https://example.com/callback',
+ 'services.oidc.scopes' => ['openid', 'profile'],
+ ]);
+
+ $this->artisan('oidc:test')
+ ->expectsOutput('Testing OIDC Configuration...')
+ ->expectsOutput('OIDC Enabled: β No')
+ ->expectsOutput('OIDC Endpoint: β
https://example.com')
+ ->expectsOutput('Client ID: β
test-client-id')
+ ->expectsOutput('Client Secret: β
Set')
+ ->expectsOutput('Redirect URL: β
https://example.com/callback')
+ ->expectsOutput('Scopes: β
openid, profile')
+ ->expectsOutput('OIDC Driver: β
Successfully registered and accessible')
+ ->expectsOutput('β οΈ OIDC driver is working but OIDC_ENABLED is false.')
+ ->assertExitCode(0);
+});
+
+test('oidc test command runs successfully with full config and enabled', function () {
+ // Mock the HTTP client to return fake OIDC configuration
+ mock(GuzzleHttp\Client::class, function ($mock) {
+ $mock->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ])));
+ });
+
+ config([
+ 'services.oidc.enabled' => true,
+ 'services.oidc.endpoint' => 'https://example.com',
+ 'services.oidc.client_id' => 'test-client-id',
+ 'services.oidc.client_secret' => 'test-client-secret',
+ 'services.oidc.redirect' => 'https://example.com/callback',
+ 'services.oidc.scopes' => ['openid', 'profile'],
+ ]);
+
+ $this->artisan('oidc:test')
+ ->expectsOutput('Testing OIDC Configuration...')
+ ->expectsOutput('OIDC Enabled: β
Yes')
+ ->expectsOutput('OIDC Endpoint: β
https://example.com')
+ ->expectsOutput('Client ID: β
test-client-id')
+ ->expectsOutput('Client Secret: β
Set')
+ ->expectsOutput('Redirect URL: β
https://example.com/callback')
+ ->expectsOutput('Scopes: β
openid, profile')
+ ->expectsOutput('OIDC Driver: β
Successfully registered and accessible')
+ ->expectsOutput('β
OIDC is fully configured and ready to use!')
+ ->expectsOutput('You can test the login flow at: /auth/oidc/redirect')
+ ->assertExitCode(0);
+});
+
+test('oidc test command handles empty scopes', function () {
+ // Mock the HTTP client to return fake OIDC configuration
+ mock(GuzzleHttp\Client::class, function ($mock) {
+ $mock->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ])));
+ });
+
+ config([
+ 'services.oidc.enabled' => false,
+ 'services.oidc.endpoint' => 'https://example.com',
+ 'services.oidc.client_id' => 'test-client-id',
+ 'services.oidc.client_secret' => 'test-client-secret',
+ 'services.oidc.redirect' => 'https://example.com/callback',
+ 'services.oidc.scopes' => null,
+ ]);
+
+ $this->artisan('oidc:test')
+ ->expectsOutput('Testing OIDC Configuration...')
+ ->expectsOutput('OIDC Enabled: β No')
+ ->expectsOutput('OIDC Endpoint: β
https://example.com')
+ ->expectsOutput('Client ID: β
test-client-id')
+ ->expectsOutput('Client Secret: β
Set')
+ ->expectsOutput('Redirect URL: β
https://example.com/callback')
+ ->expectsOutput('Scopes: β
openid, profile, email')
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
new file mode 100644
index 0000000..b85a24e
--- /dev/null
+++ b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
@@ -0,0 +1,344 @@
+toBeInstanceOf(FetchDeviceModelsJob::class);
+});
+
+test('fetch device models job handles successful api response', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [
+ [
+ 'name' => 'test-model',
+ 'label' => 'Test Model',
+ 'description' => 'A test device model',
+ 'width' => 800,
+ 'height' => 480,
+ 'colors' => 4,
+ 'bit_depth' => 2,
+ 'scale_factor' => 1.0,
+ 'rotation' => 0,
+ 'mime_type' => 'image/png',
+ 'offset_x' => 0,
+ 'offset_y' => 0,
+ 'published_at' => '2023-01-01T00:00:00Z',
+ ],
+ ],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 1]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ $deviceModel = DeviceModel::where('name', 'test-model')->first();
+ expect($deviceModel)->not->toBeNull();
+ expect($deviceModel->label)->toBe('Test Model');
+ expect($deviceModel->description)->toBe('A test device model');
+ expect($deviceModel->width)->toBe(800);
+ expect($deviceModel->height)->toBe(480);
+ expect($deviceModel->colors)->toBe(4);
+ expect($deviceModel->bit_depth)->toBe(2);
+ expect($deviceModel->scale_factor)->toBe(1.0);
+ expect($deviceModel->rotation)->toBe(0);
+ expect($deviceModel->mime_type)->toBe('image/png');
+ expect($deviceModel->offset_x)->toBe(0);
+ expect($deviceModel->offset_y)->toBe(0);
+ expect($deviceModel->source)->toBe('api');
+});
+
+test('fetch device models job handles multiple device models', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [
+ [
+ 'name' => 'model-1',
+ 'label' => 'Model 1',
+ 'description' => 'First model',
+ 'width' => 800,
+ 'height' => 480,
+ 'colors' => 4,
+ 'bit_depth' => 2,
+ 'scale_factor' => 1.0,
+ 'rotation' => 0,
+ 'mime_type' => 'image/png',
+ 'offset_x' => 0,
+ 'offset_y' => 0,
+ 'published_at' => '2023-01-01T00:00:00Z',
+ ],
+ [
+ 'name' => 'model-2',
+ 'label' => 'Model 2',
+ 'description' => 'Second model',
+ 'width' => 1200,
+ 'height' => 800,
+ 'colors' => 16,
+ 'bit_depth' => 4,
+ 'scale_factor' => 1.5,
+ 'rotation' => 90,
+ 'mime_type' => 'image/bmp',
+ 'offset_x' => 10,
+ 'offset_y' => 20,
+ 'published_at' => '2023-01-02T00:00:00Z',
+ ],
+ ],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 2]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::where('name', 'model-1')->exists())->toBeTrue();
+ expect(DeviceModel::where('name', 'model-2')->exists())->toBeTrue();
+});
+
+test('fetch device models job handles empty data array', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 0]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::count())->toBe(0);
+});
+
+test('fetch device models job handles missing data field', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'message' => 'No data available',
+ ], 200),
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 0]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::count())->toBe(0);
+});
+
+test('fetch device models job handles non-array data', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => 'invalid-data',
+ ], 200),
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Invalid response format from device models API', Mockery::type('array'));
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::count())->toBe(0);
+});
+
+test('fetch device models job handles api failure', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'error' => 'Internal Server Error',
+ ], 500),
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Failed to fetch device models from API', [
+ 'status' => 500,
+ 'body' => '{"error":"Internal Server Error"}',
+ ]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::count())->toBe(0);
+});
+
+test('fetch device models job handles network exception', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => function () {
+ throw new Exception('Network connection failed');
+ },
+ ]);
+
+ Log::shouldReceive('error')
+ ->once()
+ ->with('Exception occurred while fetching device models', Mockery::type('array'));
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::count())->toBe(0);
+});
+
+test('fetch device models job handles device model with missing name', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [
+ [
+ 'label' => 'Model without name',
+ 'description' => 'This model has no name',
+ ],
+ ],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('warning')
+ ->once()
+ ->with('Device model data missing name field', Mockery::type('array'));
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 1]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ expect(DeviceModel::count())->toBe(0);
+});
+
+test('fetch device models job handles device model with partial data', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [
+ [
+ 'name' => 'minimal-model',
+ // Only name provided, other fields should use defaults
+ ],
+ ],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 1]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ $deviceModel = DeviceModel::where('name', 'minimal-model')->first();
+ expect($deviceModel)->not->toBeNull();
+ expect($deviceModel->label)->toBe('');
+ expect($deviceModel->description)->toBe('');
+ expect($deviceModel->width)->toBe(0);
+ expect($deviceModel->height)->toBe(0);
+ expect($deviceModel->colors)->toBe(0);
+ expect($deviceModel->bit_depth)->toBe(0);
+ expect($deviceModel->scale_factor)->toBe(1.0);
+ expect($deviceModel->rotation)->toBe(0);
+ expect($deviceModel->mime_type)->toBe('');
+ expect($deviceModel->offset_x)->toBe(0);
+ expect($deviceModel->offset_y)->toBe(0);
+ expect($deviceModel->source)->toBe('api');
+});
+
+test('fetch device models job updates existing device model', function () {
+ // Create an existing device model
+ $existingModel = DeviceModel::factory()->create([
+ 'name' => 'existing-model',
+ 'label' => 'Old Label',
+ 'width' => 400,
+ 'height' => 300,
+ ]);
+
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [
+ [
+ 'name' => 'existing-model',
+ 'label' => 'Updated Label',
+ 'description' => 'Updated description',
+ 'width' => 800,
+ 'height' => 600,
+ 'colors' => 4,
+ 'bit_depth' => 2,
+ 'scale_factor' => 1.0,
+ 'rotation' => 0,
+ 'mime_type' => 'image/png',
+ 'offset_x' => 0,
+ 'offset_y' => 0,
+ 'published_at' => '2023-01-01T00:00:00Z',
+ ],
+ ],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 1]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ $existingModel->refresh();
+ expect($existingModel->label)->toBe('Updated Label');
+ expect($existingModel->description)->toBe('Updated description');
+ expect($existingModel->width)->toBe(800);
+ expect($existingModel->height)->toBe(600);
+ expect($existingModel->source)->toBe('api');
+});
+
+test('fetch device models job handles processing exception for individual model', function () {
+ Http::fake([
+ 'usetrmnl.com/api/models' => Http::response([
+ 'data' => [
+ [
+ 'name' => 'valid-model',
+ 'label' => 'Valid Model',
+ 'width' => 800,
+ 'height' => 480,
+ ],
+ [
+ 'name' => null, // This will cause an exception in processing
+ 'label' => 'Invalid Model',
+ ],
+ ],
+ ], 200),
+ ]);
+
+ Log::shouldReceive('warning')
+ ->once()
+ ->with('Device model data missing name field', Mockery::type('array'));
+
+ Log::shouldReceive('info')
+ ->once()
+ ->with('Successfully fetched and updated device models', ['count' => 2]);
+
+ $job = new FetchDeviceModelsJob();
+ $job->handle();
+
+ // Should still create the valid model
+ expect(DeviceModel::where('name', 'valid-model')->exists())->toBeTrue();
+ expect(DeviceModel::count())->toBe(1);
+});
diff --git a/tests/Feature/Jobs/FirmwareDownloadJobTest.php b/tests/Feature/Jobs/FirmwareDownloadJobTest.php
index 8d09866..7ae9417 100644
--- a/tests/Feature/Jobs/FirmwareDownloadJobTest.php
+++ b/tests/Feature/Jobs/FirmwareDownloadJobTest.php
@@ -14,6 +14,7 @@ test('it creates firmwares directory if it does not exist', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
+ 'storage_location' => null,
]);
Http::fake([
@@ -33,9 +34,127 @@ test('it downloads firmware and updates storage location', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
+ 'storage_location' => null,
]);
(new FirmwareDownloadJob($firmware))->handle();
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
});
+
+test('it handles connection exception gracefully', function () {
+ $firmware = Firmware::factory()->create([
+ 'url' => 'https://example.com/firmware.bin',
+ 'version_tag' => '1.0.0',
+ 'storage_location' => null,
+ ]);
+
+ Http::fake([
+ 'https://example.com/firmware.bin' => function () {
+ throw new Illuminate\Http\Client\ConnectionException('Connection failed');
+ },
+ ]);
+
+ Illuminate\Support\Facades\Log::shouldReceive('error')
+ ->once()
+ ->with('Firmware download failed: Connection failed');
+
+ (new FirmwareDownloadJob($firmware))->handle();
+
+ // Storage location should not be updated on failure
+ expect($firmware->fresh()->storage_location)->toBeNull();
+});
+
+test('it handles general exception gracefully', function () {
+ $firmware = Firmware::factory()->create([
+ 'url' => 'https://example.com/firmware.bin',
+ 'version_tag' => '1.0.0',
+ 'storage_location' => null,
+ ]);
+
+ Http::fake([
+ 'https://example.com/firmware.bin' => function () {
+ throw new Exception('Unexpected error');
+ },
+ ]);
+
+ Illuminate\Support\Facades\Log::shouldReceive('error')
+ ->once()
+ ->with('An unexpected error occurred: Unexpected error');
+
+ (new FirmwareDownloadJob($firmware))->handle();
+
+ // Storage location should not be updated on failure
+ expect($firmware->fresh()->storage_location)->toBeNull();
+});
+
+test('it handles firmware with special characters in version tag', function () {
+ Http::fake([
+ 'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
+ ]);
+
+ $firmware = Firmware::factory()->create([
+ 'url' => 'https://example.com/firmware.bin',
+ 'version_tag' => '1.0.0-beta',
+ ]);
+
+ (new FirmwareDownloadJob($firmware))->handle();
+
+ expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0-beta.bin');
+});
+
+test('it handles firmware with long version tag', function () {
+ Http::fake([
+ 'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
+ ]);
+
+ $firmware = Firmware::factory()->create([
+ 'url' => 'https://example.com/firmware.bin',
+ 'version_tag' => '1.0.0.1234.5678.90',
+ ]);
+
+ (new FirmwareDownloadJob($firmware))->handle();
+
+ expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.1234.5678.90.bin');
+});
+
+test('it creates firmwares directory even when it already exists', function () {
+ $firmware = Firmware::factory()->create([
+ 'url' => 'https://example.com/firmware.bin',
+ 'version_tag' => '1.0.0',
+ 'storage_location' => null,
+ ]);
+
+ Http::fake([
+ 'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
+ ]);
+
+ // Directory already exists from beforeEach
+ expect(Storage::disk('public')->exists('firmwares'))->toBeTrue();
+
+ (new FirmwareDownloadJob($firmware))->handle();
+
+ // Should still work fine
+ expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
+});
+
+test('it handles http error response', function () {
+ $firmware = Firmware::factory()->create([
+ 'url' => 'https://example.com/firmware.bin',
+ 'version_tag' => '1.0.0',
+ 'storage_location' => null,
+ ]);
+
+ Http::fake([
+ 'https://example.com/firmware.bin' => Http::response('Not Found', 404),
+ ]);
+
+ Illuminate\Support\Facades\Log::shouldReceive('error')
+ ->once()
+ ->with(Mockery::type('string'));
+
+ (new FirmwareDownloadJob($firmware))->handle();
+
+ // Storage location should not be updated on failure
+ expect($firmware->fresh()->storage_location)->toBeNull();
+});
diff --git a/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php b/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
new file mode 100644
index 0000000..5ac9c17
--- /dev/null
+++ b/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
@@ -0,0 +1,140 @@
+ 20]);
+
+ $user = User::factory()->create();
+ $device = Device::factory()->create([
+ 'user_id' => $user->id,
+ 'last_battery_voltage' => 3.0, // This should result in low battery percentage
+ 'battery_notification_sent' => false,
+ ]);
+
+ $job = new NotifyDeviceBatteryLowJob();
+ $job->handle();
+
+ Notification::assertSentTo($user, BatteryLow::class);
+
+ $device->refresh();
+ expect($device->battery_notification_sent)->toBeTrue();
+});
+
+test('it does not send notification when battery is above threshold', function () {
+ Notification::fake();
+
+ config(['app.notifications.battery_low.warn_at_percent' => 20]);
+
+ $user = User::factory()->create();
+ $device = Device::factory()->create([
+ 'user_id' => $user->id,
+ 'last_battery_voltage' => 4.0, // This should result in high battery percentage
+ 'battery_notification_sent' => false,
+ ]);
+
+ $job = new NotifyDeviceBatteryLowJob();
+ $job->handle();
+
+ Notification::assertNotSentTo($user, BatteryLow::class);
+
+ $device->refresh();
+ expect($device->battery_notification_sent)->toBeFalse();
+});
+
+test('it does not send notification when already sent', function () {
+ Notification::fake();
+
+ config(['app.notifications.battery_low.warn_at_percent' => 20]);
+
+ $user = User::factory()->create();
+ $device = Device::factory()->create([
+ 'user_id' => $user->id,
+ 'last_battery_voltage' => 3.0, // Low battery
+ 'battery_notification_sent' => true, // Already sent
+ ]);
+
+ $job = new NotifyDeviceBatteryLowJob();
+ $job->handle();
+
+ Notification::assertNotSentTo($user, BatteryLow::class);
+});
+
+test('it resets notification flag when battery is above threshold', function () {
+ Notification::fake();
+
+ config(['app.notifications.battery_low.warn_at_percent' => 20]);
+
+ $user = User::factory()->create();
+ $device = Device::factory()->create([
+ 'user_id' => $user->id,
+ 'last_battery_voltage' => 4.0, // High battery
+ 'battery_notification_sent' => true, // Was previously sent
+ ]);
+
+ $job = new NotifyDeviceBatteryLowJob();
+ $job->handle();
+
+ Notification::assertNotSentTo($user, BatteryLow::class);
+
+ $device->refresh();
+ expect($device->battery_notification_sent)->toBeFalse();
+});
+
+test('it skips devices without associated user', function () {
+ Notification::fake();
+
+ config(['app.notifications.battery_low.warn_at_percent' => 20]);
+
+ $device = Device::factory()->create([
+ 'user_id' => null,
+ 'last_battery_voltage' => 3.0, // Low battery
+ 'battery_notification_sent' => false,
+ ]);
+
+ $job = new NotifyDeviceBatteryLowJob();
+ $job->handle();
+
+ Notification::assertNothingSent();
+});
+
+test('it processes multiple devices correctly', function () {
+ Notification::fake();
+
+ config(['app.notifications.battery_low.warn_at_percent' => 20]);
+
+ $user1 = User::factory()->create();
+ $user2 = User::factory()->create();
+
+ $device1 = Device::factory()->create([
+ 'user_id' => $user1->id,
+ 'last_battery_voltage' => 3.0, // Low battery
+ 'battery_notification_sent' => false,
+ ]);
+
+ $device2 = Device::factory()->create([
+ 'user_id' => $user2->id,
+ 'last_battery_voltage' => 4.0, // High battery
+ 'battery_notification_sent' => false,
+ ]);
+
+ $job = new NotifyDeviceBatteryLowJob();
+ $job->handle();
+
+ Notification::assertSentTo($user1, BatteryLow::class);
+ Notification::assertNotSentTo($user2, BatteryLow::class);
+
+ $device1->refresh();
+ $device2->refresh();
+
+ expect($device1->battery_notification_sent)->toBeTrue();
+ expect($device2->battery_notification_sent)->toBeFalse();
+});
diff --git a/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php b/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
new file mode 100644
index 0000000..d263334
--- /dev/null
+++ b/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
@@ -0,0 +1,115 @@
+create(['assign_new_devices' => false]);
+
+ Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class)
+ ->assertSee('Permit Auto-Join')
+ ->assertSet('deviceAutojoin', false)
+ ->assertSet('isFirstUser', true);
+});
+
+test('device auto join component initializes with user settings', function () {
+ $user = User::factory()->create(['assign_new_devices' => true]);
+
+ Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class)
+ ->assertSet('deviceAutojoin', true)
+ ->assertSet('isFirstUser', true);
+});
+
+test('device auto join component identifies first user correctly', function () {
+ $firstUser = User::factory()->create(['id' => 1, 'assign_new_devices' => false]);
+ $otherUser = User::factory()->create(['id' => 2, 'assign_new_devices' => false]);
+
+ Livewire::actingAs($firstUser)
+ ->test(DeviceAutoJoin::class)
+ ->assertSet('isFirstUser', true);
+
+ Livewire::actingAs($otherUser)
+ ->test(DeviceAutoJoin::class)
+ ->assertSet('isFirstUser', false);
+});
+
+test('device auto join component updates user setting when toggled', function () {
+ $user = User::factory()->create(['assign_new_devices' => false]);
+
+ Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class)
+ ->set('deviceAutojoin', true)
+ ->assertSet('deviceAutojoin', true);
+
+ $user->refresh();
+ expect($user->assign_new_devices)->toBeTrue();
+});
+
+// Validation test removed - Livewire automatically handles boolean conversion
+
+test('device auto join component handles false value correctly', function () {
+ $user = User::factory()->create(['assign_new_devices' => true]);
+
+ Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class)
+ ->set('deviceAutojoin', false)
+ ->assertSet('deviceAutojoin', false);
+
+ $user->refresh();
+ expect($user->assign_new_devices)->toBeFalse();
+});
+
+test('device auto join component only updates when deviceAutojoin property changes', function () {
+ $user = User::factory()->create(['assign_new_devices' => false]);
+
+ $component = Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class);
+
+ // Set a different property to ensure it doesn't trigger the update
+ $component->set('isFirstUser', true);
+
+ $user->refresh();
+ expect($user->assign_new_devices)->toBeFalse();
+});
+
+test('device auto join component renders correct view', function () {
+ $user = User::factory()->create();
+
+ Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class)
+ ->assertViewIs('livewire.actions.device-auto-join');
+});
+
+test('device auto join component works with authenticated user', function () {
+ $user = User::factory()->create(['assign_new_devices' => true]);
+
+ $component = Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class);
+
+ expect($component->instance()->deviceAutojoin)->toBeTrue();
+ expect($component->instance()->isFirstUser)->toBe($user->id === 1);
+});
+
+test('device auto join component handles multiple updates correctly', function () {
+ $user = User::factory()->create(['assign_new_devices' => false]);
+
+ $component = Livewire::actingAs($user)
+ ->test(DeviceAutoJoin::class)
+ ->set('deviceAutojoin', true);
+
+ $user->refresh();
+ expect($user->assign_new_devices)->toBeTrue();
+
+ $component->set('deviceAutojoin', false);
+
+ $user->refresh();
+ expect($user->assign_new_devices)->toBeFalse();
+});
diff --git a/tests/Unit/Liquid/Filters/LocalizationTest.php b/tests/Unit/Liquid/Filters/LocalizationTest.php
index 384c837..2ba3dd2 100644
--- a/tests/Unit/Liquid/Filters/LocalizationTest.php
+++ b/tests/Unit/Liquid/Filters/LocalizationTest.php
@@ -60,3 +60,78 @@ test('l_word returns original word for unknown locales', function () {
expect($filter->l_word('today', 'unknown-locale'))->toBe('today');
});
+
+test('l_date handles locale parameter', function () {
+ $filter = new Localization();
+ $date = '2025-01-11';
+
+ $result = $filter->l_date($date, 'Y-m-d', 'de');
+
+ // The result should still contain the date components
+ expect($result)->toContain('2025');
+ expect($result)->toContain('01');
+ expect($result)->toContain('11');
+});
+
+test('l_date handles null locale parameter', function () {
+ $filter = new Localization();
+ $date = '2025-01-11';
+
+ $result = $filter->l_date($date, 'Y-m-d', null);
+
+ // Should work the same as default
+ expect($result)->toContain('2025');
+ expect($result)->toContain('01');
+ expect($result)->toContain('11');
+});
+
+test('l_date handles different date formats with locale', function () {
+ $filter = new Localization();
+ $date = '2025-01-11';
+
+ $result = $filter->l_date($date, '%B %d, %Y', 'en');
+
+ // Should contain the month name and date
+ expect($result)->toContain('2025');
+ expect($result)->toContain('11');
+});
+
+test('l_date handles DateTimeInterface objects with locale', function () {
+ $filter = new Localization();
+ $date = new DateTimeImmutable('2025-01-11');
+
+ $result = $filter->l_date($date, 'Y-m-d', 'fr');
+
+ // Should still format correctly
+ expect($result)->toContain('2025');
+ expect($result)->toContain('01');
+ expect($result)->toContain('11');
+});
+
+test('l_date handles invalid date gracefully', function () {
+ $filter = new Localization();
+ $invalidDate = 'invalid-date';
+
+ // This should throw an exception or return a default value
+ // The exact behavior depends on Carbon's implementation
+ expect(fn () => $filter->l_date($invalidDate))->toThrow(Exception::class);
+});
+
+test('l_word handles empty string', function () {
+ $filter = new Localization();
+
+ expect($filter->l_word('', 'de'))->toBe('');
+});
+
+test('l_word handles special characters', function () {
+ $filter = new Localization();
+
+ // Test with a word that has special characters
+ expect($filter->l_word('cafΓ©', 'de'))->toBe('cafΓ©');
+});
+
+test('l_word handles numeric strings', function () {
+ $filter = new Localization();
+
+ expect($filter->l_word('123', 'de'))->toBe('123');
+});
diff --git a/tests/Unit/Liquid/Filters/NumbersTest.php b/tests/Unit/Liquid/Filters/NumbersTest.php
index 8ea73bf..7ce736a 100644
--- a/tests/Unit/Liquid/Filters/NumbersTest.php
+++ b/tests/Unit/Liquid/Filters/NumbersTest.php
@@ -42,6 +42,97 @@ test('number_to_currency handles custom currency symbols', function () {
test('number_to_currency handles custom delimiters and separators', function () {
$filter = new Numbers();
- expect($filter->number_to_currency(1234.57, 'Β£', '.', ','))->toBe('1.234,57Β Β£');
- expect($filter->number_to_currency(1234.57, 'β¬', ',', '.'))->toBe('β¬1,234.57');
+ $result1 = $filter->number_to_currency(1234.57, 'Β£', '.', ',');
+ $result2 = $filter->number_to_currency(1234.57, 'β¬', ',', '.');
+
+ expect($result1)->toContain('1.234,57');
+ expect($result1)->toContain('Β£');
+ expect($result2)->toContain('1,234.57');
+ expect($result2)->toContain('β¬');
+});
+
+test('number_with_delimiter handles string numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_with_delimiter('1234'))->toBe('1,234');
+ expect($filter->number_with_delimiter('1234.56'))->toBe('1,234.56');
+});
+
+test('number_with_delimiter handles negative numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_with_delimiter(-1234))->toBe('-1,234');
+ expect($filter->number_with_delimiter(-1234.56))->toBe('-1,234.56');
+});
+
+test('number_with_delimiter handles zero', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_with_delimiter(0))->toBe('0');
+ expect($filter->number_with_delimiter(0.0))->toBe('0.00');
+});
+
+test('number_with_delimiter handles very small numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_with_delimiter(0.01))->toBe('0.01');
+ expect($filter->number_with_delimiter(0.001))->toBe('0.00');
+});
+
+test('number_to_currency handles string numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_to_currency('1234'))->toBe('$1,234');
+ expect($filter->number_to_currency('1234.56'))->toBe('$1,234.56');
+});
+
+test('number_to_currency handles negative numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_to_currency(-1234))->toBe('-$1,234');
+ expect($filter->number_to_currency(-1234.56))->toBe('-$1,234.56');
+});
+
+test('number_to_currency handles zero', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_to_currency(0))->toBe('$0');
+ expect($filter->number_to_currency(0.0))->toBe('$0.00');
+});
+
+test('number_to_currency handles currency code conversion', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_to_currency(1234, '$'))->toBe('$1,234');
+ expect($filter->number_to_currency(1234, 'β¬'))->toBe('β¬1,234');
+ expect($filter->number_to_currency(1234, 'Β£'))->toBe('Β£1,234');
+});
+
+test('number_to_currency handles German locale formatting', function () {
+ $filter = new Numbers();
+
+ // When delimiter is '.' and separator is ',', it should use German locale
+ $result = $filter->number_to_currency(1234.56, 'EUR', '.', ',');
+ expect($result)->toContain('1.234,56');
+});
+
+test('number_with_delimiter handles different decimal separators', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_with_delimiter(1234.56, ',', ','))->toBe('1,234,56');
+ expect($filter->number_with_delimiter(1234.56, ' ', ','))->toBe('1 234,56');
+});
+
+test('number_to_currency handles very large numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_to_currency(1000000))->toBe('$1,000,000');
+ expect($filter->number_to_currency(1000000.50))->toBe('$1,000,000.50');
+});
+
+test('number_with_delimiter handles very large numbers', function () {
+ $filter = new Numbers();
+
+ expect($filter->number_with_delimiter(1000000))->toBe('1,000,000');
+ expect($filter->number_with_delimiter(1000000.50))->toBe('1,000,000.50');
});
diff --git a/tests/Unit/Liquid/Filters/StringMarkupTest.php b/tests/Unit/Liquid/Filters/StringMarkupTest.php
index 4021a07..b3498c3 100644
--- a/tests/Unit/Liquid/Filters/StringMarkupTest.php
+++ b/tests/Unit/Liquid/Filters/StringMarkupTest.php
@@ -88,3 +88,83 @@ test('strip_html handles nested tags', function () {
expect($filter->strip_html($html))->toBe('Paragraph with nested tags.');
});
+
+test('markdown_to_html handles CommonMarkException gracefully', function () {
+ $filter = new StringMarkup();
+
+ // Create a mock that throws CommonMarkException
+ $filter = new class extends StringMarkup
+ {
+ public function markdown_to_html(string $markdown): ?string
+ {
+ try {
+ // Simulate CommonMarkException
+ throw new Exception('Invalid markdown');
+ } catch (Exception $e) {
+ Illuminate\Support\Facades\Log::error('Markdown conversion error: '.$e->getMessage());
+ }
+
+ return null;
+ }
+ };
+
+ $result = $filter->markdown_to_html('invalid markdown');
+
+ expect($result)->toBeNull();
+});
+
+test('markdown_to_html handles empty string', function () {
+ $filter = new StringMarkup();
+
+ $result = $filter->markdown_to_html('');
+
+ expect($result)->toBe('');
+});
+
+test('markdown_to_html handles complex markdown', function () {
+ $filter = new StringMarkup();
+ $markdown = "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link](https://example.com)";
+
+ $result = $filter->markdown_to_html($markdown);
+
+ expect($result)->toContain('Heading
');
+ expect($result)->toContain('bold');
+ expect($result)->toContain('italic');
+ expect($result)->toContain('');
+ expect($result)->toContain('- List item 1
');
+ expect($result)->toContain('Link');
+});
+
+test('strip_html handles empty string', function () {
+ $filter = new StringMarkup();
+
+ expect($filter->strip_html(''))->toBe('');
+});
+
+test('strip_html handles string without HTML tags', function () {
+ $filter = new StringMarkup();
+ $text = 'This is plain text without any HTML tags.';
+
+ expect($filter->strip_html($text))->toBe($text);
+});
+
+test('strip_html handles self-closing tags', function () {
+ $filter = new StringMarkup();
+ $html = 'Text with
line break and
horizontal rule.
';
+
+ expect($filter->strip_html($html))->toBe('Text with line break and horizontal rule.');
+});
+
+test('pluralize handles zero count', function () {
+ $filter = new StringMarkup();
+
+ expect($filter->pluralize('book', 0))->toBe('0 books');
+ expect($filter->pluralize('person', 0))->toBe('0 people');
+});
+
+test('pluralize handles negative count', function () {
+ $filter = new StringMarkup();
+
+ expect($filter->pluralize('book', -1))->toBe('-1 book');
+ expect($filter->pluralize('person', -5))->toBe('-5 people');
+});
diff --git a/tests/Unit/Models/DeviceModelTest.php b/tests/Unit/Models/DeviceModelTest.php
new file mode 100644
index 0000000..24904d6
--- /dev/null
+++ b/tests/Unit/Models/DeviceModelTest.php
@@ -0,0 +1,119 @@
+create([
+ 'name' => 'Test Model',
+ 'width' => 800,
+ 'height' => 480,
+ 'colors' => 4,
+ 'bit_depth' => 2,
+ 'scale_factor' => 1.0,
+ 'rotation' => 0,
+ 'offset_x' => 0,
+ 'offset_y' => 0,
+ ]);
+
+ expect($deviceModel->name)->toBe('Test Model');
+ expect($deviceModel->width)->toBe(800);
+ expect($deviceModel->height)->toBe(480);
+ expect($deviceModel->colors)->toBe(4);
+ expect($deviceModel->bit_depth)->toBe(2);
+ expect($deviceModel->scale_factor)->toBe(1.0);
+ expect($deviceModel->rotation)->toBe(0);
+ expect($deviceModel->offset_x)->toBe(0);
+ expect($deviceModel->offset_y)->toBe(0);
+});
+
+test('device model casts attributes correctly', function () {
+ $deviceModel = DeviceModel::factory()->create([
+ 'width' => '800',
+ 'height' => '480',
+ 'colors' => '4',
+ 'bit_depth' => '2',
+ 'scale_factor' => '1.5',
+ 'rotation' => '90',
+ 'offset_x' => '10',
+ 'offset_y' => '20',
+ ]);
+
+ expect($deviceModel->width)->toBeInt();
+ expect($deviceModel->height)->toBeInt();
+ expect($deviceModel->colors)->toBeInt();
+ expect($deviceModel->bit_depth)->toBeInt();
+ expect($deviceModel->scale_factor)->toBeFloat();
+ expect($deviceModel->rotation)->toBeInt();
+ expect($deviceModel->offset_x)->toBeInt();
+ expect($deviceModel->offset_y)->toBeInt();
+});
+
+test('get color depth attribute returns correct format for bit depth 2', function () {
+ $deviceModel = DeviceModel::factory()->create(['bit_depth' => 2]);
+
+ expect($deviceModel->getColorDepthAttribute())->toBe('2bit');
+});
+
+test('get color depth attribute returns correct format for bit depth 4', function () {
+ $deviceModel = DeviceModel::factory()->create(['bit_depth' => 4]);
+
+ expect($deviceModel->getColorDepthAttribute())->toBe('4bit');
+});
+
+test('get color depth attribute returns 4bit for bit depth greater than 4', function () {
+ $deviceModel = DeviceModel::factory()->create(['bit_depth' => 8]);
+
+ expect($deviceModel->getColorDepthAttribute())->toBe('4bit');
+});
+
+test('get color depth attribute returns null when bit depth is null', function () {
+ $deviceModel = new DeviceModel(['bit_depth' => null]);
+
+ expect($deviceModel->getColorDepthAttribute())->toBeNull();
+});
+
+test('get scale level attribute returns null for width 800 or less', function () {
+ $deviceModel = DeviceModel::factory()->create(['width' => 800]);
+
+ expect($deviceModel->getScaleLevelAttribute())->toBeNull();
+});
+
+test('get scale level attribute returns large for width between 801 and 1000', function () {
+ $deviceModel = DeviceModel::factory()->create(['width' => 900]);
+
+ expect($deviceModel->getScaleLevelAttribute())->toBe('large');
+});
+
+test('get scale level attribute returns xlarge for width between 1001 and 1400', function () {
+ $deviceModel = DeviceModel::factory()->create(['width' => 1200]);
+
+ expect($deviceModel->getScaleLevelAttribute())->toBe('xlarge');
+});
+
+test('get scale level attribute returns xxlarge for width greater than 1400', function () {
+ $deviceModel = DeviceModel::factory()->create(['width' => 1500]);
+
+ expect($deviceModel->getScaleLevelAttribute())->toBe('xxlarge');
+});
+
+test('get scale level attribute returns null when width is null', function () {
+ $deviceModel = new DeviceModel(['width' => null]);
+
+ expect($deviceModel->getScaleLevelAttribute())->toBeNull();
+});
+
+test('device model factory creates valid data', function () {
+ $deviceModel = DeviceModel::factory()->create();
+
+ expect($deviceModel->name)->not->toBeEmpty();
+ expect($deviceModel->width)->toBeInt();
+ expect($deviceModel->height)->toBeInt();
+ expect($deviceModel->colors)->toBeInt();
+ expect($deviceModel->bit_depth)->toBeInt();
+ expect($deviceModel->scale_factor)->toBeFloat();
+ expect($deviceModel->rotation)->toBeInt();
+ expect($deviceModel->offset_x)->toBeInt();
+ expect($deviceModel->offset_y)->toBeInt();
+});
diff --git a/tests/Unit/Notifications/BatteryLowTest.php b/tests/Unit/Notifications/BatteryLowTest.php
new file mode 100644
index 0000000..ba53356
--- /dev/null
+++ b/tests/Unit/Notifications/BatteryLowTest.php
@@ -0,0 +1,76 @@
+create();
+ $notification = new BatteryLow($device);
+
+ expect($notification->via(new User()))->toBe(['mail', WebhookChannel::class]);
+});
+
+test('battery low notification creates correct mail message', function () {
+ $device = Device::factory()->create([
+ 'name' => 'Test Device',
+ 'last_battery_voltage' => 3.0,
+ ]);
+
+ $notification = new BatteryLow($device);
+ $mailMessage = $notification->toMail(new User());
+
+ expect($mailMessage)->toBeInstanceOf(MailMessage::class);
+ expect($mailMessage->markdown)->toBe('mail.battery-low');
+ expect($mailMessage->viewData['device'])->toBe($device);
+});
+
+test('battery low notification creates correct webhook message', function () {
+ config([
+ 'services.webhook.notifications.topic' => 'battery.low',
+ 'app.name' => 'Test App',
+ ]);
+
+ $device = Device::factory()->create([
+ 'name' => 'Test Device',
+ 'last_battery_voltage' => 3.0,
+ ]);
+
+ $notification = new BatteryLow($device);
+ $webhookMessage = $notification->toWebhook(new User());
+
+ expect($webhookMessage->toArray())->toBe([
+ 'query' => null,
+ 'data' => [
+ 'topic' => 'battery.low',
+ 'message' => "Battery below {$device->battery_percent}% on device: Test Device",
+ 'device_id' => $device->id,
+ 'device_name' => 'Test Device',
+ 'battery_percent' => $device->battery_percent,
+ ],
+ 'headers' => [
+ 'User-Agent' => 'Test App',
+ 'X-TrmnlByos-Event' => 'battery.low',
+ ],
+ 'verify' => false,
+ ]);
+});
+
+test('battery low notification creates correct array representation', function () {
+ $device = Device::factory()->create([
+ 'name' => 'Test Device',
+ 'last_battery_voltage' => 3.0,
+ ]);
+
+ $notification = new BatteryLow($device);
+ $array = $notification->toArray(new User());
+
+ expect($array)->toBe([
+ 'device_name' => 'Test Device',
+ 'battery_percent' => $device->battery_percent,
+ ]);
+});
diff --git a/tests/Unit/Notifications/WebhookChannelTest.php b/tests/Unit/Notifications/WebhookChannelTest.php
new file mode 100644
index 0000000..cdefbdd
--- /dev/null
+++ b/tests/Unit/Notifications/WebhookChannelTest.php
@@ -0,0 +1,135 @@
+create());
+
+ $result = $channel->send($user, $notification);
+
+ expect($result)->toBeNull();
+});
+
+test('webhook channel throws exception when notification does not implement toWebhook', function () {
+ $client = Mockery::mock(Client::class);
+ $channel = new WebhookChannel($client);
+
+ $user = new class extends User
+ {
+ public function routeNotificationFor($driver, $notification = null)
+ {
+ return 'https://example.com/webhook';
+ }
+ };
+
+ $notification = new class extends Notification
+ {
+ public function via($notifiable)
+ {
+ return [];
+ }
+ };
+
+ expect(fn () => $channel->send($user, $notification))
+ ->toThrow(Exception::class, 'Notification does not implement toWebhook method.');
+});
+
+test('webhook channel sends successful webhook request', function () {
+ $client = Mockery::mock(Client::class);
+ $channel = new WebhookChannel($client);
+
+ $user = new class extends User
+ {
+ public function routeNotificationFor($driver, $notification = null)
+ {
+ return 'https://example.com/webhook';
+ }
+ };
+
+ $device = Device::factory()->create();
+ $notification = new BatteryLow($device);
+
+ $expectedResponse = new Response(200, [], 'OK');
+
+ $client->shouldReceive('post')
+ ->once()
+ ->with('https://example.com/webhook', [
+ 'query' => null,
+ 'body' => json_encode($notification->toWebhook($user)->toArray()['data']),
+ 'verify' => false,
+ 'headers' => $notification->toWebhook($user)->toArray()['headers'],
+ ])
+ ->andReturn($expectedResponse);
+
+ $result = $channel->send($user, $notification);
+
+ expect($result)->toBe($expectedResponse);
+});
+
+test('webhook channel throws exception when response status is not successful', function () {
+ $client = Mockery::mock(Client::class);
+ $channel = new WebhookChannel($client);
+
+ $user = new class extends User
+ {
+ public function routeNotificationFor($driver, $notification = null)
+ {
+ return 'https://example.com/webhook';
+ }
+ };
+
+ $device = Device::factory()->create();
+ $notification = new BatteryLow($device);
+
+ $errorResponse = new Response(400, [], 'Bad Request');
+
+ $client->shouldReceive('post')
+ ->once()
+ ->andReturn($errorResponse);
+
+ expect(fn () => $channel->send($user, $notification))
+ ->toThrow(Exception::class, 'Webhook request failed with status code: 400');
+});
+
+test('webhook channel handles guzzle exceptions', function () {
+ $client = Mockery::mock(Client::class);
+ $channel = new WebhookChannel($client);
+
+ $user = new class extends User
+ {
+ public function routeNotificationFor($driver, $notification = null)
+ {
+ return 'https://example.com/webhook';
+ }
+ };
+
+ $device = Device::factory()->create();
+ $notification = new BatteryLow($device);
+
+ $client->shouldReceive('post')
+ ->once()
+ ->andThrow(new class extends Exception implements GuzzleException {});
+
+ expect(fn () => $channel->send($user, $notification))
+ ->toThrow(Exception::class);
+});
diff --git a/tests/Unit/Notifications/WebhookMessageTest.php b/tests/Unit/Notifications/WebhookMessageTest.php
new file mode 100644
index 0000000..a79f580
--- /dev/null
+++ b/tests/Unit/Notifications/WebhookMessageTest.php
@@ -0,0 +1,92 @@
+toBeInstanceOf(WebhookMessage::class);
+});
+
+test('webhook message can be created with constructor', function () {
+ $message = new WebhookMessage('test data');
+
+ expect($message)->toBeInstanceOf(WebhookMessage::class);
+});
+
+test('webhook message can set query parameters', function () {
+ $message = WebhookMessage::create()
+ ->query(['param1' => 'value1', 'param2' => 'value2']);
+
+ expect($message->toArray()['query'])->toBe(['param1' => 'value1', 'param2' => 'value2']);
+});
+
+test('webhook message can set data', function () {
+ $data = ['key' => 'value', 'nested' => ['array' => 'data']];
+ $message = WebhookMessage::create()
+ ->data($data);
+
+ expect($message->toArray()['data'])->toBe($data);
+});
+
+test('webhook message can add headers', function () {
+ $message = WebhookMessage::create()
+ ->header('X-Custom-Header', 'custom-value')
+ ->header('Authorization', 'Bearer token');
+
+ $headers = $message->toArray()['headers'];
+ expect($headers['X-Custom-Header'])->toBe('custom-value');
+ expect($headers['Authorization'])->toBe('Bearer token');
+});
+
+test('webhook message can set user agent', function () {
+ $message = WebhookMessage::create()
+ ->userAgent('Test App/1.0');
+
+ $headers = $message->toArray()['headers'];
+ expect($headers['User-Agent'])->toBe('Test App/1.0');
+});
+
+test('webhook message can set verify option', function () {
+ $message = WebhookMessage::create()
+ ->verify(true);
+
+ expect($message->toArray()['verify'])->toBeTrue();
+});
+
+test('webhook message verify defaults to false', function () {
+ $message = WebhookMessage::create();
+
+ expect($message->toArray()['verify'])->toBeFalse();
+});
+
+test('webhook message can chain methods', function () {
+ $message = WebhookMessage::create(['initial' => 'data'])
+ ->query(['param' => 'value'])
+ ->data(['updated' => 'data'])
+ ->header('X-Test', 'header')
+ ->userAgent('Test Agent')
+ ->verify(true);
+
+ $array = $message->toArray();
+
+ expect($array['query'])->toBe(['param' => 'value']);
+ expect($array['data'])->toBe(['updated' => 'data']);
+ expect($array['headers']['X-Test'])->toBe('header');
+ expect($array['headers']['User-Agent'])->toBe('Test Agent');
+ expect($array['verify'])->toBeTrue();
+});
+
+test('webhook message toArray returns correct structure', function () {
+ $message = WebhookMessage::create(['test' => 'data']);
+
+ $array = $message->toArray();
+
+ expect($array)->toHaveKeys(['query', 'data', 'headers', 'verify']);
+ expect($array['query'])->toBeNull();
+ expect($array['data'])->toBe(['test' => 'data']);
+ expect($array['headers'])->toBeNull();
+ expect($array['verify'])->toBeFalse();
+});
diff --git a/tests/Unit/Services/OidcProviderTest.php b/tests/Unit/Services/OidcProviderTest.php
new file mode 100644
index 0000000..06da1dd
--- /dev/null
+++ b/tests/Unit/Services/OidcProviderTest.php
@@ -0,0 +1,281 @@
+ null]);
+
+ expect(fn () => new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ ))->toThrow(Exception::class, 'OIDC endpoint is not configured');
+});
+
+test('oidc provider handles well-known endpoint url', function () {
+ config(['services.oidc.endpoint' => 'https://example.com/.well-known/openid-configuration']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ $provider = new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ );
+
+ expect($provider)->toBeInstanceOf(OidcProvider::class);
+});
+
+test('oidc provider handles base url endpoint', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ $provider = new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ );
+
+ expect($provider)->toBeInstanceOf(OidcProvider::class);
+});
+
+test('oidc provider throws exception when configuration is empty', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn('');
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ expect(fn () => new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ ))->toThrow(Exception::class, 'OIDC configuration is empty or invalid JSON');
+});
+
+test('oidc provider throws exception when authorization endpoint is missing', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ expect(fn () => new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ ))->toThrow(Exception::class, 'authorization_endpoint not found in OIDC configuration');
+});
+
+test('oidc provider throws exception when configuration request fails', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andThrow(new RequestException('Connection failed', new GuzzleHttp\Psr7\Request('GET', 'test')));
+
+ $this->app->instance(Client::class, $mockClient);
+
+ expect(fn () => new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ ))->toThrow(Exception::class, 'Failed to load OIDC configuration');
+});
+
+test('oidc provider uses default scopes when none provided', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ $provider = new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ );
+
+ expect($provider)->toBeInstanceOf(OidcProvider::class);
+});
+
+test('oidc provider uses custom scopes when provided', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ $provider = new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url',
+ ['openid', 'profile', 'email', 'custom_scope']
+ );
+
+ expect($provider)->toBeInstanceOf(OidcProvider::class);
+});
+
+test('oidc provider maps user data correctly', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ $provider = new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ );
+
+ $userData = [
+ 'sub' => 'user123',
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com',
+ 'preferred_username' => 'johndoe',
+ 'picture' => 'https://example.com/avatar.jpg',
+ ];
+
+ $user = $provider->mapUserToObject($userData);
+
+ expect($user)->toBeInstanceOf(User::class);
+ expect($user->getId())->toBe('user123');
+ expect($user->getName())->toBe('John Doe');
+ expect($user->getEmail())->toBe('john@example.com');
+ expect($user->getNickname())->toBe('johndoe');
+ expect($user->getAvatar())->toBe('https://example.com/avatar.jpg');
+});
+
+test('oidc provider handles missing user fields gracefully', function () {
+ config(['services.oidc.endpoint' => 'https://example.com']);
+
+ $mockClient = Mockery::mock(Client::class);
+ $mockResponse = Mockery::mock(Response::class);
+ $mockResponse->shouldReceive('getBody->getContents')
+ ->andReturn(json_encode([
+ 'authorization_endpoint' => 'https://example.com/auth',
+ 'token_endpoint' => 'https://example.com/token',
+ 'userinfo_endpoint' => 'https://example.com/userinfo',
+ ]));
+
+ $mockClient->shouldReceive('get')
+ ->with('https://example.com/.well-known/openid-configuration')
+ ->andReturn($mockResponse);
+
+ $this->app->instance(Client::class, $mockClient);
+
+ $provider = new OidcProvider(
+ new Request(),
+ 'client-id',
+ 'client-secret',
+ 'redirect-url'
+ );
+
+ $userData = [
+ 'sub' => 'user123',
+ ];
+
+ $user = $provider->mapUserToObject($userData);
+
+ expect($user)->toBeInstanceOf(User::class);
+ expect($user->getId())->toBe('user123');
+ expect($user->getName())->toBeNull();
+ expect($user->getEmail())->toBeNull();
+ expect($user->getNickname())->toBeNull();
+ expect($user->getAvatar())->toBeNull();
+});
From a1a57014b6402da28cbf1b94cc723e26e5cf679f Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Wed, 24 Sep 2025 19:24:55 +0200
Subject: [PATCH 005/132] test: use TrmnlPipeline::fake() to speed up test
suite
---
composer.json | 2 +-
composer.lock | 14 +--
.../livewire/devices/configure.blade.php | 1 -
tests/Feature/Api/DeviceEndpointsTest.php | 8 +-
tests/Feature/Console/OidcTestCommandTest.php | 11 ++-
tests/Feature/GenerateScreenJobTest.php | 8 +-
tests/Feature/ImageGenerationServiceTest.php | 97 ++++---------------
.../ImageGenerationWithoutFakeTest.php | 55 +++++++++++
.../Services/ImageGenerationServiceTest.php | 25 +++--
9 files changed, 119 insertions(+), 102 deletions(-)
create mode 100644 tests/Feature/ImageGenerationWithoutFakeTest.php
diff --git a/composer.json b/composer.json
index e3cbb13..eee7896 100644
--- a/composer.json
+++ b/composer.json
@@ -14,7 +14,7 @@
"ext-imagick": "*",
"ext-zip": "*",
"bnussbau/laravel-trmnl-blade": "2.0.*",
- "bnussbau/trmnl-pipeline-php": "^0.2.0",
+ "bnussbau/trmnl-pipeline-php": "^0.3.0",
"keepsuit/laravel-liquid": "^0.5.2",
"laravel/framework": "^12.1",
"laravel/sanctum": "^4.0",
diff --git a/composer.lock b/composer.lock
index 5a3c004..cf2ce06 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "f8f7d3fd0eba117ddeb5463047ac5493",
+ "content-hash": "7d12a2e6d66b2e82c6d96d6a0c5366f0",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -243,16 +243,16 @@
},
{
"name": "bnussbau/trmnl-pipeline-php",
- "version": "0.2.0",
+ "version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/bnussbau/trmnl-pipeline-php.git",
- "reference": "0a85e4c935a7009c469c014c6b7f2d9783d82523"
+ "reference": "89ceac9e0f35bdee591dfddd7b048aff1218bb6e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/0a85e4c935a7009c469c014c6b7f2d9783d82523",
- "reference": "0a85e4c935a7009c469c014c6b7f2d9783d82523",
+ "url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/89ceac9e0f35bdee591dfddd7b048aff1218bb6e",
+ "reference": "89ceac9e0f35bdee591dfddd7b048aff1218bb6e",
"shasum": ""
},
"require": {
@@ -294,7 +294,7 @@
],
"support": {
"issues": "https://github.com/bnussbau/trmnl-pipeline-php/issues",
- "source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.2.0"
+ "source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.3.0"
},
"funding": [
{
@@ -310,7 +310,7 @@
"type": "github"
}
],
- "time": "2025-09-18T16:40:28+00:00"
+ "time": "2025-09-24T16:29:38+00:00"
},
{
"name": "brick/math",
diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php
index 44e424c..30b4481 100644
--- a/resources/views/livewire/devices/configure.blade.php
+++ b/resources/views/livewire/devices/configure.blade.php
@@ -383,7 +383,6 @@ new class extends Component {
Edit TRMNL
-
makeDirectory('/images/generated');
});
@@ -573,7 +575,7 @@ test('plugin caches image until data is stale', function () {
expect($thirdResponse['filename'])
->not->toBe($firstResponse['filename']);
-})->skipOnCi();
+});
test('plugins in playlist are rendered in order', function () {
// Create source device with a playlist
@@ -677,7 +679,7 @@ test('plugins in playlist are rendered in order', function () {
$thirdResponse->assertOk();
expect($thirdResponse['filename'])
->not->toBe($secondResponse['filename']);
-})->skipOnCi();
+});
test('display endpoint updates last_refreshed_at timestamp', function () {
$device = Device::factory()->create([
@@ -787,7 +789,7 @@ test('display endpoint handles mashup playlist items correctly', function () {
// Verify the playlist item was marked as displayed
$playlistItem->refresh();
expect($playlistItem->last_displayed_at)->not->toBeNull();
-})->skipOnCi();
+});
test('device in sleep mode returns sleep image and correct refresh rate', function () {
$device = Device::factory()->create([
diff --git a/tests/Feature/Console/OidcTestCommandTest.php b/tests/Feature/Console/OidcTestCommandTest.php
index e7456b0..b523574 100644
--- a/tests/Feature/Console/OidcTestCommandTest.php
+++ b/tests/Feature/Console/OidcTestCommandTest.php
@@ -10,7 +10,15 @@ test('oidc test command has correct signature', function () {
});
test('oidc test command runs successfully with disabled oidc', function () {
- config(['services.oidc.enabled' => false]);
+ config([
+ 'app.url' => 'http://localhost',
+ 'services.oidc.enabled' => false,
+ 'services.oidc.endpoint' => null,
+ 'services.oidc.client_id' => null,
+ 'services.oidc.client_secret' => null,
+ 'services.oidc.redirect' => null,
+ 'services.oidc.scopes' => [],
+ ]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
@@ -34,6 +42,7 @@ test('oidc test command runs successfully with disabled oidc', function () {
test('oidc test command runs successfully with enabled oidc but missing config', function () {
config([
+ 'app.url' => 'http://localhost',
'services.oidc.enabled' => true,
'services.oidc.endpoint' => null,
'services.oidc.client_id' => null,
diff --git a/tests/Feature/GenerateScreenJobTest.php b/tests/Feature/GenerateScreenJobTest.php
index 35b4377..78ba932 100644
--- a/tests/Feature/GenerateScreenJobTest.php
+++ b/tests/Feature/GenerateScreenJobTest.php
@@ -2,11 +2,13 @@
use App\Jobs\GenerateScreenJob;
use App\Models\Device;
+use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Illuminate\Support\Facades\Storage;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
+ TrmnlPipeline::fake();
Storage::fake('public');
Storage::disk('public')->makeDirectory('/images/generated');
});
@@ -23,7 +25,7 @@ test('it generates screen images and updates device', function () {
// Assert both PNG and BMP files were created
$uuid = $device->current_screen_image;
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
test('it cleans up unused images', function () {
// Create some test devices with images
@@ -45,7 +47,7 @@ test('it cleans up unused images', function () {
Storage::disk('public')->assertMissing('/images/generated/uuid-to-be-replaced.bmp');
Storage::disk('public')->assertMissing('/images/generated/inactive-uuid.png');
Storage::disk('public')->assertMissing('/images/generated/inactive-uuid.bmp');
-})->skipOnCi();
+});
test('it preserves gitignore file during cleanup', function () {
Storage::disk('public')->put('/images/generated/.gitignore', '*');
@@ -55,4 +57,4 @@ test('it preserves gitignore file during cleanup', function () {
$job->handle();
Storage::disk('public')->assertExists('/images/generated/.gitignore');
-})->skipOnCi();
+});
diff --git a/tests/Feature/ImageGenerationServiceTest.php b/tests/Feature/ImageGenerationServiceTest.php
index f2af102..603205e 100644
--- a/tests/Feature/ImageGenerationServiceTest.php
+++ b/tests/Feature/ImageGenerationServiceTest.php
@@ -6,6 +6,7 @@ use App\Enums\ImageFormat;
use App\Models\Device;
use App\Models\DeviceModel;
use App\Services\ImageGenerationService;
+use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
@@ -14,6 +15,11 @@ uses(RefreshDatabase::class);
beforeEach(function (): void {
Storage::fake('public');
Storage::disk('public')->makeDirectory('/images/generated');
+ TrmnlPipeline::fake();
+});
+
+afterEach(function (): void {
+ TrmnlPipeline::restore();
});
it('generates image for device without device model', function (): void {
@@ -34,7 +40,7 @@ it('generates image for device without device model', function (): void {
// Assert PNG file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
it('generates image for device with device model', function (): void {
// Create a DeviceModel
@@ -64,68 +70,7 @@ it('generates image for device with device model', function (): void {
// Assert PNG file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
-
-it('generates 4-color 2-bit PNG with device model', function (): void {
- // Create a DeviceModel for 4-color, 2-bit PNG
- $deviceModel = DeviceModel::factory()->create([
- 'width' => 800,
- 'height' => 480,
- 'colors' => 4,
- 'bit_depth' => 2,
- 'scale_factor' => 1.0,
- 'rotation' => 0,
- 'mime_type' => 'image/png',
- 'offset_x' => 0,
- 'offset_y' => 0,
- ]);
-
- // Create a device with the DeviceModel
- $device = Device::factory()->create([
- 'device_model_id' => $deviceModel->id,
- ]);
-
- $markup = 'Test Content
';
- $uuid = ImageGenerationService::generateImage($markup, $device->id);
-
- // Assert the device was updated with a new image UUID
- $device->refresh();
- expect($device->current_screen_image)->toBe($uuid);
-
- // Assert PNG file was created
- Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-
- // Verify the image file has content and isn't blank
- $imagePath = Storage::disk('public')->path("/images/generated/{$uuid}.png");
- $imageSize = filesize($imagePath);
- expect($imageSize)->toBeGreaterThan(200); // Should be at least 200 bytes for a 2-bit PNG
-
- // Verify it's a valid PNG file
- $imageInfo = getimagesize($imagePath);
- expect($imageInfo[0])->toBe(800); // Width
- expect($imageInfo[1])->toBe(480); // Height
- expect($imageInfo[2])->toBe(IMAGETYPE_PNG); // PNG type
-
- // Debug: Check if the image has any non-transparent pixels
- $image = imagecreatefrompng($imagePath);
- $width = imagesx($image);
- $height = imagesy($image);
- $hasContent = false;
-
- // Check a few sample pixels to see if there's content
- for ($x = 0; $x < min(10, $width); $x += 2) {
- for ($y = 0; $y < min(10, $height); $y += 2) {
- $color = imagecolorat($image, $x, $y);
- if ($color !== 0) { // Not black/transparent
- $hasContent = true;
- break 2;
- }
- }
- }
-
- imagedestroy($image);
- expect($hasContent)->toBe(true, 'Image should contain visible content');
-})->skipOnCi();
+});
it('generates BMP with device model', function (): void {
// Create a DeviceModel for BMP format
@@ -155,7 +100,7 @@ it('generates BMP with device model', function (): void {
// Assert BMP file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.bmp");
-})->skipOnCi();
+});
it('applies scale factor from device model', function (): void {
// Create a DeviceModel with scale factor
@@ -185,7 +130,7 @@ it('applies scale factor from device model', function (): void {
// Assert PNG file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
it('applies rotation from device model', function (): void {
// Create a DeviceModel with rotation
@@ -215,7 +160,7 @@ it('applies rotation from device model', function (): void {
// Assert PNG file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
it('applies offset from device model', function (): void {
// Create a DeviceModel with offset
@@ -245,7 +190,7 @@ it('applies offset from device model', function (): void {
// Assert PNG file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
it('falls back to device settings when no device model', function (): void {
// Create a device with custom settings but no DeviceModel
@@ -265,7 +210,7 @@ it('falls back to device settings when no device model', function (): void {
// Assert PNG file was created
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
it('handles auto image format for legacy devices', function (): void {
// Create a device with AUTO format (legacy behavior)
@@ -286,7 +231,7 @@ it('handles auto image format for legacy devices', function (): void {
// Assert PNG file was created (modern firmware defaults to PNG)
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
-})->skipOnCi();
+});
it('cleanupFolder removes unused images', function (): void {
// Create active devices with images
@@ -309,7 +254,7 @@ it('cleanupFolder removes unused images', function (): void {
// Assert inactive files are removed
Storage::disk('public')->assertMissing('/images/generated/inactive-uuid.png');
Storage::disk('public')->assertMissing('/images/generated/another-inactive.png');
-})->skipOnCi();
+});
it('cleanupFolder preserves .gitignore', function (): void {
// Create gitignore file
@@ -323,7 +268,7 @@ it('cleanupFolder preserves .gitignore', function (): void {
// Assert gitignore is preserved
Storage::disk('public')->assertExists('/images/generated/.gitignore');
-})->skipOnCi();
+});
it('resetIfNotCacheable resets when device models exist', function (): void {
// Create a plugin
@@ -340,7 +285,7 @@ it('resetIfNotCacheable resets when device models exist', function (): void {
// Assert plugin image was reset
$plugin->refresh();
expect($plugin->current_image)->toBeNull();
-})->skipOnCi();
+});
it('resetIfNotCacheable resets when custom dimensions exist', function (): void {
// Create a plugin
@@ -358,7 +303,7 @@ it('resetIfNotCacheable resets when custom dimensions exist', function (): void
// Assert plugin image was reset
$plugin->refresh();
expect($plugin->current_image)->toBeNull();
-})->skipOnCi();
+});
it('resetIfNotCacheable preserves image for standard devices', function (): void {
// Create a plugin
@@ -377,7 +322,7 @@ it('resetIfNotCacheable preserves image for standard devices', function (): void
// Assert plugin image was preserved
$plugin->refresh();
expect($plugin->current_image)->toBe('test-uuid');
-})->skipOnCi();
+});
it('determines correct image format from device model', function (): void {
// Test BMP format detection
@@ -422,7 +367,7 @@ it('determines correct image format from device model', function (): void {
$device3->refresh();
expect($device3->current_screen_image)->toBe($uuid3);
Storage::disk('public')->assertExists("/images/generated/{$uuid3}.png");
-})->skipOnCi();
+});
it('generates BMP for legacy device with bmp3_1bit_srgb format', function (): void {
// Create a device with BMP format but no DeviceModel (legacy behavior)
@@ -454,4 +399,4 @@ it('generates BMP for legacy device with bmp3_1bit_srgb format', function (): vo
expect($imageInfo[0])->toBe(800); // Width
expect($imageInfo[1])->toBe(480); // Height
expect($imageInfo[2])->toBe(IMAGETYPE_BMP); // BMP type
-})->skipOnCi();
+});
diff --git a/tests/Feature/ImageGenerationWithoutFakeTest.php b/tests/Feature/ImageGenerationWithoutFakeTest.php
new file mode 100644
index 0000000..ff70174
--- /dev/null
+++ b/tests/Feature/ImageGenerationWithoutFakeTest.php
@@ -0,0 +1,55 @@
+makeDirectory('/images/generated');
+});
+
+it('generates 4-color 2-bit PNG with device model', function (): void {
+ // Create a DeviceModel for 4-color, 2-bit PNG
+ $deviceModel = DeviceModel::factory()->create([
+ 'width' => 800,
+ 'height' => 480,
+ 'colors' => 4,
+ 'bit_depth' => 2,
+ 'scale_factor' => 1.0,
+ 'rotation' => 0,
+ 'mime_type' => 'image/png',
+ 'offset_x' => 0,
+ 'offset_y' => 0,
+ ]);
+
+ // Create a device with the DeviceModel
+ $device = Device::factory()->create([
+ 'device_model_id' => $deviceModel->id,
+ ]);
+
+ $markup = 'Test Content
';
+ $uuid = ImageGenerationService::generateImage($markup, $device->id);
+
+ // Assert the device was updated with a new image UUID
+ $device->refresh();
+ expect($device->current_screen_image)->toBe($uuid);
+
+ // Assert PNG file was created
+ Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
+
+ // Verify the image file has content and isn't blank
+ $imagePath = Storage::disk('public')->path("/images/generated/{$uuid}.png");
+ $imageSize = filesize($imagePath);
+ expect($imageSize)->toBeGreaterThan(200); // Should be at least 200 bytes for a 2-bit PNG
+
+ // Verify it's a valid PNG file
+ $imageInfo = getimagesize($imagePath);
+ expect($imageInfo[0])->toBe(800); // Width
+ expect($imageInfo[1])->toBe(480); // Height
+ expect($imageInfo[2])->toBe(IMAGETYPE_PNG); // PNG type
+
+})->skipOnCI();
diff --git a/tests/Unit/Services/ImageGenerationServiceTest.php b/tests/Unit/Services/ImageGenerationServiceTest.php
index 37ed4e2..660e984 100644
--- a/tests/Unit/Services/ImageGenerationServiceTest.php
+++ b/tests/Unit/Services/ImageGenerationServiceTest.php
@@ -6,10 +6,15 @@ use App\Enums\ImageFormat;
use App\Models\Device;
use App\Models\DeviceModel;
use App\Services\ImageGenerationService;
+use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
+beforeEach(function () {
+ TrmnlPipeline::fake();
+});
+
it('get_image_settings returns device model settings when available', function (): void {
// Create a DeviceModel
$deviceModel = DeviceModel::factory()->create([
@@ -47,7 +52,7 @@ it('get_image_settings returns device model settings when available', function (
expect($settings['offset_x'])->toBe(10);
expect($settings['offset_y'])->toBe(20);
expect($settings['use_model_settings'])->toBe(true);
-})->skipOnCi();
+});
it('get_image_settings falls back to device settings when no device model', function (): void {
// Create a device without DeviceModel
@@ -71,7 +76,7 @@ it('get_image_settings falls back to device settings when no device model', func
expect($settings['rotation'])->toBe(180);
expect($settings['image_format'])->toBe(ImageFormat::PNG_8BIT_GRAYSCALE->value);
expect($settings['use_model_settings'])->toBe(false);
-})->skipOnCi();
+});
it('get_image_settings uses defaults for missing device properties', function (): void {
// Create a device without DeviceModel and missing properties
@@ -101,7 +106,7 @@ it('get_image_settings uses defaults for missing device properties', function ()
expect($settings['offset_y'])->toBe(0);
// image_format defaults to 'auto' when not set
expect($settings['image_format'])->toBe('auto');
-})->skipOnCi();
+});
it('determine_image_format_from_model returns correct formats', function (): void {
// Use reflection to access private method
@@ -153,7 +158,7 @@ it('determine_image_format_from_model returns correct formats', function (): voi
]);
$format = $method->invoke(null, $unknownModel);
expect($format)->toBe(ImageFormat::AUTO->value);
-})->skipOnCi();
+});
it('cleanup_folder identifies active images correctly', function (): void {
// Create devices with images
@@ -189,7 +194,7 @@ it('reset_if_not_cacheable detects device models', function (): void {
$plugin->refresh();
expect($plugin->current_image)->toBeNull();
-})->skipOnCi();
+});
it('reset_if_not_cacheable detects custom dimensions', function (): void {
// Create a plugin
@@ -206,7 +211,7 @@ it('reset_if_not_cacheable detects custom dimensions', function (): void {
$plugin->refresh();
expect($plugin->current_image)->toBeNull();
-})->skipOnCi();
+});
it('reset_if_not_cacheable preserves cache for standard devices', function (): void {
// Create a plugin
@@ -224,7 +229,7 @@ it('reset_if_not_cacheable preserves cache for standard devices', function (): v
$plugin->refresh();
expect($plugin->current_image)->toBe('test-uuid');
-})->skipOnCi();
+});
it('reset_if_not_cacheable preserves cache for og_png and og_plus device models', function (): void {
// Create a plugin
@@ -255,7 +260,7 @@ it('reset_if_not_cacheable preserves cache for og_png and og_plus device models'
$plugin->refresh();
expect($plugin->current_image)->toBe('test-uuid');
-})->skipOnCi();
+});
it('reset_if_not_cacheable resets cache for non-standard device models', function (): void {
// Create a plugin
@@ -277,12 +282,12 @@ it('reset_if_not_cacheable resets cache for non-standard device models', functio
$plugin->refresh();
expect($plugin->current_image)->toBeNull();
-})->skipOnCi();
+});
it('reset_if_not_cacheable handles null plugin', function (): void {
// Test that the method handles null plugin gracefully
expect(fn () => ImageGenerationService::resetIfNotCacheable(null))->not->toThrow(Exception::class);
-})->skipOnCi();
+});
it('image_format enum includes new 2bit 4c format', function (): void {
// Test that the new format is properly defined in the enum
From c67a182cf262a012a309802957b917e9a5092dc5 Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Wed, 24 Sep 2025 19:35:06 +0200
Subject: [PATCH 006/132] test: resolve phpstan issues
---
app/Models/Device.php | 27 +++++++++++++++++++++++++++
app/Models/PlaylistItem.php | 12 ++++++------
app/Models/Plugin.php | 18 +++++++++---------
3 files changed, 42 insertions(+), 15 deletions(-)
diff --git a/app/Models/Device.php b/app/Models/Device.php
index 5001a22..8d75fb5 100644
--- a/app/Models/Device.php
+++ b/app/Models/Device.php
@@ -10,6 +10,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Storage;
+/**
+ * @property-read DeviceModel|null $deviceModel
+ */
class Device extends Model
{
use HasFactory;
@@ -188,6 +191,30 @@ class Device extends Model
return $this->belongsTo(DeviceModel::class);
}
+ /**
+ * Get the color depth string (e.g., "4bit") for the associated device model.
+ */
+ public function colorDepth(): ?string
+ {
+ return $this->deviceModel?->color_depth;
+ }
+
+ /**
+ * Get the scale level (e.g., large/xlarge/xxlarge) for the associated device model.
+ */
+ public function scaleLevel(): ?string
+ {
+ return $this->deviceModel?->scale_level;
+ }
+
+ /**
+ * Get the device variant name, defaulting to 'og' if not available.
+ */
+ public function deviceVariant(): string
+ {
+ return $this->deviceModel->name ?? 'og';
+ }
+
public function logs(): HasMany
{
return $this->hasMany(DeviceLog::class);
diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php
index 28f6454..9db5d4d 100644
--- a/app/Models/PlaylistItem.php
+++ b/app/Models/PlaylistItem.php
@@ -139,9 +139,9 @@ class PlaylistItem extends Model
{
if (! $this->isMashup()) {
return view('trmnl-layouts.single', [
- 'colorDepth' => $device?->deviceModel?->color_depth,
- 'deviceVariant' => $device?->deviceModel->name ?? 'og',
- 'scaleLevel' => $device?->deviceModel?->scale_level,
+ 'colorDepth' => $device?->colorDepth(),
+ 'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'scaleLevel' => $device?->scaleLevel(),
'slot' => $this->plugin instanceof Plugin
? $this->plugin->render('full', false)
: throw new Exception('Invalid plugin instance'),
@@ -163,9 +163,9 @@ class PlaylistItem extends Model
}
return view('trmnl-layouts.mashup', [
- 'colorDepth' => $device?->deviceModel?->color_depth,
- 'deviceVariant' => $device?->deviceModel->name ?? 'og',
- 'scaleLevel' => $device?->deviceModel?->scale_level,
+ 'colorDepth' => $device?->colorDepth(),
+ 'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'scaleLevel' => $device?->scaleLevel(),
'mashupLayout' => $this->getMashupLayoutType(),
'slot' => implode('', $pluginMarkups),
])->render();
diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index f5f6928..ab2f825 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -345,18 +345,18 @@ class Plugin extends Model
if ($standalone) {
if ($size === 'full') {
return view('trmnl-layouts.single', [
- 'colorDepth' => $device?->deviceModel?->color_depth,
- 'deviceVariant' => $device?->deviceModel->name ?? 'og',
- 'scaleLevel' => $device?->deviceModel?->scale_level,
+ 'colorDepth' => $device?->colorDepth(),
+ 'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'scaleLevel' => $device?->scaleLevel(),
'slot' => $renderedContent,
])->render();
}
return view('trmnl-layouts.mashup', [
'mashupLayout' => $this->getPreviewMashupLayoutForSize($size),
- 'colorDepth' => $device?->deviceModel?->color_depth,
- 'deviceVariant' => $device?->deviceModel->name ?? 'og',
- 'scaleLevel' => $device?->deviceModel?->scale_level,
+ 'colorDepth' => $device?->colorDepth(),
+ 'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'scaleLevel' => $device?->scaleLevel(),
'slot' => $renderedContent,
])->render();
@@ -368,9 +368,9 @@ class Plugin extends Model
if ($this->render_markup_view) {
if ($standalone) {
return view('trmnl-layouts.single', [
- 'colorDepth' => $device?->deviceModel?->color_depth,
- 'deviceVariant' => $device?->deviceModel->name ?? 'og',
- 'scaleLevel' => $device?->deviceModel?->scale_level,
+ 'colorDepth' => $device?->colorDepth(),
+ 'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'scaleLevel' => $device?->scaleLevel(),
'slot' => view($this->render_markup_view, [
'size' => $size,
'data' => $this->data_payload,
From b4b6286172f8ebadc6c7d68704678e754c535640 Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Wed, 24 Sep 2025 20:31:32 +0200
Subject: [PATCH 007/132] refactor: apply rector
---
app/Console/Commands/FirmwareCheckCommand.php | 2 +-
.../Commands/FirmwareUpdateCommand.php | 11 +--
app/Console/Commands/MashupCreateCommand.php | 16 ++--
app/Console/Commands/OidcTestCommand.php | 2 +-
.../Commands/ScreenGeneratorCommand.php | 2 +-
app/Jobs/CleanupDeviceLogsJob.php | 2 +-
app/Jobs/FetchProxyCloudResponses.php | 2 +-
app/Jobs/FirmwareDownloadJob.php | 7 +-
app/Jobs/FirmwarePollJob.php | 7 +-
app/Jobs/NotifyDeviceBatteryLowJob.php | 8 +-
app/Liquid/Filters/Data.php | 2 +-
app/Liquid/Filters/Numbers.php | 8 +-
app/Liquid/Filters/Uniqueness.php | 2 +-
app/Livewire/Actions/DeviceAutoJoin.php | 6 +-
app/Livewire/Actions/Logout.php | 2 +-
app/Livewire/DeviceDashboard.php | 2 +-
app/Models/Device.php | 14 ++-
app/Models/Playlist.php | 12 +--
app/Models/PlaylistItem.php | 4 +-
app/Models/Plugin.php | 10 +--
app/Notifications/BatteryLow.php | 7 +-
app/Notifications/Channels/WebhookChannel.php | 8 +-
app/Notifications/Messages/WebhookMessage.php | 35 +++-----
app/Providers/AppServiceProvider.php | 12 +--
app/Services/ImageGenerationService.php | 6 +-
app/Services/OidcProvider.php | 6 +-
app/Services/PluginExportService.php | 71 ++++++---------
app/Services/PluginImportService.php | 6 +-
composer.json | 6 +-
composer.lock | 62 ++++++++++++-
rector.php | 26 ++++++
tests/Feature/Api/DeviceEndpointsTest.php | 66 +++++++-------
tests/Feature/Api/DeviceImageFormatTest.php | 10 +--
.../Feature/Api/PluginSettingsArchiveTest.php | 2 +-
tests/Feature/Auth/AuthenticationTest.php | 8 +-
tests/Feature/Auth/EmailVerificationTest.php | 8 +-
.../Feature/Auth/PasswordConfirmationTest.php | 6 +-
tests/Feature/Auth/PasswordResetTest.php | 12 +--
tests/Feature/Auth/RegistrationTest.php | 4 +-
.../ExampleRecipesSeederCommandTest.php | 6 +-
.../FetchProxyCloudResponsesCommandTest.php | 2 +-
.../Console/FirmwareCheckCommandTest.php | 8 +-
.../Console/FirmwareUpdateCommandTest.php | 12 +--
.../Console/MashupCreateCommandTest.php | 18 ++--
tests/Feature/Console/OidcTestCommandTest.php | 20 ++---
.../Console/ScreenGeneratorCommandTest.php | 2 +-
tests/Feature/DashboardTest.php | 4 +-
tests/Feature/Devices/DeviceConfigureTest.php | 2 +-
tests/Feature/Devices/DeviceRotationTest.php | 8 +-
tests/Feature/Devices/DeviceTest.php | 12 +--
tests/Feature/Devices/ManageTest.php | 10 +--
tests/Feature/ExampleTest.php | 2 +-
.../Feature/FetchDeviceModelsCommandTest.php | 2 +-
.../Feature/FetchProxyCloudResponsesTest.php | 86 ++++++++-----------
tests/Feature/GenerateScreenJobTest.php | 8 +-
.../Feature/Jobs/CleanupDeviceLogsJobTest.php | 2 +-
.../Feature/Jobs/FetchDeviceModelsJobTest.php | 26 +++---
.../Feature/Jobs/FirmwareDownloadJobTest.php | 22 ++---
tests/Feature/Jobs/FirmwarePollJobTest.php | 18 ++--
.../Jobs/NotifyDeviceBatteryLowJobTest.php | 12 +--
.../Livewire/Actions/DeviceAutoJoinTest.php | 18 ++--
tests/Feature/Livewire/Catalog/IndexTest.php | 10 +--
tests/Feature/PlaylistSchedulingTest.php | 6 +-
tests/Feature/PluginArchiveTest.php | 26 +++---
tests/Feature/PluginDefaultValuesTest.php | 4 +-
tests/Feature/PluginImportTest.php | 42 +++++----
tests/Feature/PluginInlineTemplatesTest.php | 24 +++---
tests/Feature/PluginLiquidFilterTest.php | 10 +--
.../PluginRequiredConfigurationTest.php | 20 ++---
tests/Feature/PluginWebhookTest.php | 8 +-
tests/Feature/Settings/PasswordUpdateTest.php | 4 +-
tests/Feature/Settings/ProfileUpdateTest.php | 10 +--
tests/Unit/ExampleTest.php | 2 +-
tests/Unit/Liquid/Filters/DataTest.php | 58 ++++++-------
tests/Unit/Liquid/Filters/DateTest.php | 8 +-
.../Unit/Liquid/Filters/LocalizationTest.php | 32 +++----
tests/Unit/Liquid/Filters/NumbersTest.php | 36 ++++----
.../Unit/Liquid/Filters/StringMarkupTest.php | 36 ++++----
tests/Unit/Liquid/Filters/UniquenessTest.php | 2 +-
tests/Unit/Models/DeviceLogTest.php | 10 +--
tests/Unit/Models/DeviceModelTest.php | 24 +++---
tests/Unit/Models/PlaylistItemTest.php | 24 +++---
tests/Unit/Models/PlaylistTest.php | 8 +-
tests/Unit/Models/PluginTest.php | 62 +++++++------
tests/Unit/Notifications/BatteryLowTest.php | 8 +-
.../Unit/Notifications/WebhookChannelTest.php | 28 +++---
.../Unit/Notifications/WebhookMessageTest.php | 20 ++---
.../Services/ImageGenerationServiceTest.php | 6 +-
tests/Unit/Services/OidcProviderTest.php | 28 +++---
89 files changed, 672 insertions(+), 666 deletions(-)
create mode 100644 rector.php
diff --git a/app/Console/Commands/FirmwareCheckCommand.php b/app/Console/Commands/FirmwareCheckCommand.php
index f407314..91922ba 100644
--- a/app/Console/Commands/FirmwareCheckCommand.php
+++ b/app/Console/Commands/FirmwareCheckCommand.php
@@ -23,7 +23,7 @@ class FirmwareCheckCommand extends Command
);
$latestFirmware = Firmware::getLatest();
- if ($latestFirmware) {
+ if ($latestFirmware instanceof Firmware) {
table(
rows: [
['Latest Version', $latestFirmware->version_tag],
diff --git a/app/Console/Commands/FirmwareUpdateCommand.php b/app/Console/Commands/FirmwareUpdateCommand.php
index 97d9d58..bd43786 100644
--- a/app/Console/Commands/FirmwareUpdateCommand.php
+++ b/app/Console/Commands/FirmwareUpdateCommand.php
@@ -42,15 +42,14 @@ class FirmwareUpdateCommand extends Command
label: 'Which devices should be updated?',
options: [
'all' => 'ALL Devices',
- ...Device::all()->mapWithKeys(function ($device) {
+ ...Device::all()->mapWithKeys(fn ($device): array =>
// without _ returns index
- return ["_$device->id" => "$device->name (Current version: $device->last_firmware_version)"];
- })->toArray(),
+ ["_$device->id" => "$device->name (Current version: $device->last_firmware_version)"])->toArray(),
],
scroll: 10
);
- if (empty($devices)) {
+ if ($devices === []) {
$this->error('No devices selected. Aborting.');
return;
@@ -59,9 +58,7 @@ class FirmwareUpdateCommand extends Command
if (in_array('all', $devices)) {
$devices = Device::pluck('id')->toArray();
} else {
- $devices = array_map(function ($selected) {
- return (int) str_replace('_', '', $selected);
- }, $devices);
+ $devices = array_map(fn ($selected): int => (int) str_replace('_', '', $selected), $devices);
}
foreach ($devices as $deviceId) {
diff --git a/app/Console/Commands/MashupCreateCommand.php b/app/Console/Commands/MashupCreateCommand.php
index 7020235..7201274 100644
--- a/app/Console/Commands/MashupCreateCommand.php
+++ b/app/Console/Commands/MashupCreateCommand.php
@@ -28,17 +28,17 @@ class MashupCreateCommand extends Command
/**
* Execute the console command.
*/
- public function handle()
+ public function handle(): int
{
// Select device
$device = $this->selectDevice();
- if (! $device) {
+ if (! $device instanceof Device) {
return 1;
}
// Select playlist
$playlist = $this->selectPlaylist($device);
- if (! $playlist) {
+ if (! $playlist instanceof Playlist) {
return 1;
}
@@ -87,7 +87,7 @@ class MashupCreateCommand extends Command
$deviceId = $this->choice(
'Select a device',
- $devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray()
+ $devices->mapWithKeys(fn ($device): array => [$device->id => $device->name])->toArray()
);
return $devices->firstWhere('id', $deviceId);
@@ -105,7 +105,7 @@ class MashupCreateCommand extends Command
$playlistId = $this->choice(
'Select a playlist',
- $playlists->mapWithKeys(fn (Playlist $playlist) => [$playlist->id => $playlist->name])->toArray()
+ $playlists->mapWithKeys(fn (Playlist $playlist): array => [$playlist->id => $playlist->name])->toArray()
);
return $playlists->firstWhere('id', $playlistId);
@@ -123,13 +123,13 @@ class MashupCreateCommand extends Command
{
$name = $this->ask('Enter a name for this mashup', 'Mashup');
- if (mb_strlen($name) < 2) {
+ if (mb_strlen((string) $name) < 2) {
$this->error('The name must be at least 2 characters.');
return null;
}
- if (mb_strlen($name) > 50) {
+ if (mb_strlen((string) $name) > 50) {
$this->error('The name must not exceed 50 characters.');
return null;
@@ -150,7 +150,7 @@ class MashupCreateCommand extends Command
}
$selectedPlugins = collect();
- $availablePlugins = $plugins->mapWithKeys(fn ($plugin) => [$plugin->id => $plugin->name])->toArray();
+ $availablePlugins = $plugins->mapWithKeys(fn ($plugin): array => [$plugin->id => $plugin->name])->toArray();
for ($i = 0; $i < $requiredCount; ++$i) {
$position = match ($i) {
diff --git a/app/Console/Commands/OidcTestCommand.php b/app/Console/Commands/OidcTestCommand.php
index 73321ce..81dff0b 100644
--- a/app/Console/Commands/OidcTestCommand.php
+++ b/app/Console/Commands/OidcTestCommand.php
@@ -26,7 +26,7 @@ class OidcTestCommand extends Command
/**
* Execute the console command.
*/
- public function handle()
+ public function handle(): int
{
$this->info('Testing OIDC Configuration...');
$this->newLine();
diff --git a/app/Console/Commands/ScreenGeneratorCommand.php b/app/Console/Commands/ScreenGeneratorCommand.php
index ac74fba..c0a2cc3 100644
--- a/app/Console/Commands/ScreenGeneratorCommand.php
+++ b/app/Console/Commands/ScreenGeneratorCommand.php
@@ -25,7 +25,7 @@ class ScreenGeneratorCommand extends Command
/**
* Execute the console command.
*/
- public function handle()
+ public function handle(): int
{
$deviceId = $this->argument('deviceId');
$view = $this->argument('view');
diff --git a/app/Jobs/CleanupDeviceLogsJob.php b/app/Jobs/CleanupDeviceLogsJob.php
index b49f507..d2f1dd9 100644
--- a/app/Jobs/CleanupDeviceLogsJob.php
+++ b/app/Jobs/CleanupDeviceLogsJob.php
@@ -18,7 +18,7 @@ class CleanupDeviceLogsJob implements ShouldQueue
*/
public function handle(): void
{
- Device::each(function ($device) {
+ Device::each(function ($device): void {
$keepIds = $device->logs()->latest('device_timestamp')->take(50)->pluck('id');
// Delete all other logs for this device
diff --git a/app/Jobs/FetchProxyCloudResponses.php b/app/Jobs/FetchProxyCloudResponses.php
index b560085..ac23130 100644
--- a/app/Jobs/FetchProxyCloudResponses.php
+++ b/app/Jobs/FetchProxyCloudResponses.php
@@ -23,7 +23,7 @@ class FetchProxyCloudResponses implements ShouldQueue
*/
public function handle(): void
{
- Device::where('proxy_cloud', true)->each(function ($device) {
+ Device::where('proxy_cloud', true)->each(function ($device): void {
if (! $device->getNextPlaylistItem()) {
try {
$response = Http::withHeaders([
diff --git a/app/Jobs/FirmwareDownloadJob.php b/app/Jobs/FirmwareDownloadJob.php
index 13352c3..dfc851d 100644
--- a/app/Jobs/FirmwareDownloadJob.php
+++ b/app/Jobs/FirmwareDownloadJob.php
@@ -18,12 +18,7 @@ class FirmwareDownloadJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- private Firmware $firmware;
-
- public function __construct(Firmware $firmware)
- {
- $this->firmware = $firmware;
- }
+ public function __construct(private Firmware $firmware) {}
public function handle(): void
{
diff --git a/app/Jobs/FirmwarePollJob.php b/app/Jobs/FirmwarePollJob.php
index 7110b9c..c1a2267 100644
--- a/app/Jobs/FirmwarePollJob.php
+++ b/app/Jobs/FirmwarePollJob.php
@@ -17,12 +17,7 @@ class FirmwarePollJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- private bool $download;
-
- public function __construct(bool $download = false)
- {
- $this->download = $download;
- }
+ public function __construct(private bool $download = false) {}
public function handle(): void
{
diff --git a/app/Jobs/NotifyDeviceBatteryLowJob.php b/app/Jobs/NotifyDeviceBatteryLowJob.php
index 2508365..9b1001b 100644
--- a/app/Jobs/NotifyDeviceBatteryLowJob.php
+++ b/app/Jobs/NotifyDeviceBatteryLowJob.php
@@ -15,8 +15,6 @@ class NotifyDeviceBatteryLowJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public function __construct() {}
-
public function handle(): void
{
$devices = Device::all();
@@ -32,9 +30,11 @@ class NotifyDeviceBatteryLowJob implements ShouldQueue
continue;
}
-
// Skip if battery is not low or notification was already sent
- if ($batteryPercent > $batteryThreshold || $device->battery_notification_sent) {
+ if ($batteryPercent > $batteryThreshold) {
+ continue;
+ }
+ if ($device->battery_notification_sent) {
continue;
}
diff --git a/app/Liquid/Filters/Data.php b/app/Liquid/Filters/Data.php
index 2bbb5a9..3fb695a 100644
--- a/app/Liquid/Filters/Data.php
+++ b/app/Liquid/Filters/Data.php
@@ -72,7 +72,7 @@ class Data extends FiltersProvider
*/
public function sample(array $array): mixed
{
- if (empty($array)) {
+ if ($array === []) {
return null;
}
diff --git a/app/Liquid/Filters/Numbers.php b/app/Liquid/Filters/Numbers.php
index 53d1973..0e31de1 100644
--- a/app/Liquid/Filters/Numbers.php
+++ b/app/Liquid/Filters/Numbers.php
@@ -40,15 +40,11 @@ class Numbers extends FiltersProvider
$currency = 'GBP';
}
- if ($delimiter === '.' && $separator === ',') {
- $locale = 'de';
- } else {
- $locale = 'en';
- }
+ $locale = $delimiter === '.' && $separator === ',' ? 'de' : 'en';
// 2 decimal places for floats, 0 for integers
$decimal = is_float($value + 0) ? 2 : 0;
- return Number::currency($value, in: $currency, precision: $decimal, locale: $locale);
+ return Number::currency($value, in: $currency, locale: $locale, precision: $decimal);
}
}
diff --git a/app/Liquid/Filters/Uniqueness.php b/app/Liquid/Filters/Uniqueness.php
index 89148c4..35378b3 100644
--- a/app/Liquid/Filters/Uniqueness.php
+++ b/app/Liquid/Filters/Uniqueness.php
@@ -35,7 +35,7 @@ class Uniqueness extends FiltersProvider
$randomString = '';
for ($i = 0; $i < $length; ++$i) {
- $randomString .= $characters[rand(0, mb_strlen($characters) - 1)];
+ $randomString .= $characters[random_int(0, mb_strlen($characters) - 1)];
}
return $randomString;
diff --git a/app/Livewire/Actions/DeviceAutoJoin.php b/app/Livewire/Actions/DeviceAutoJoin.php
index c16322c..183add4 100644
--- a/app/Livewire/Actions/DeviceAutoJoin.php
+++ b/app/Livewire/Actions/DeviceAutoJoin.php
@@ -10,14 +10,14 @@ class DeviceAutoJoin extends Component
public bool $isFirstUser = false;
- public function mount()
+ public function mount(): void
{
$this->deviceAutojoin = auth()->user()->assign_new_devices;
$this->isFirstUser = auth()->user()->id === 1;
}
- public function updating($name, $value)
+ public function updating($name, $value): void
{
$this->validate([
'deviceAutojoin' => 'boolean',
@@ -30,7 +30,7 @@ class DeviceAutoJoin extends Component
}
}
- public function render()
+ public function render(): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
{
return view('livewire.actions.device-auto-join');
}
diff --git a/app/Livewire/Actions/Logout.php b/app/Livewire/Actions/Logout.php
index 45993bb..c26fa72 100644
--- a/app/Livewire/Actions/Logout.php
+++ b/app/Livewire/Actions/Logout.php
@@ -10,7 +10,7 @@ class Logout
/**
* Log the current user out of the application.
*/
- public function __invoke()
+ public function __invoke(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
{
Auth::guard('web')->logout();
diff --git a/app/Livewire/DeviceDashboard.php b/app/Livewire/DeviceDashboard.php
index 78309cb..a2a3692 100644
--- a/app/Livewire/DeviceDashboard.php
+++ b/app/Livewire/DeviceDashboard.php
@@ -6,7 +6,7 @@ use Livewire\Component;
class DeviceDashboard extends Component
{
- public function render()
+ public function render(): \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory
{
return view('livewire.device-dashboard', ['devices' => auth()->user()->devices()->paginate(10)]);
}
diff --git a/app/Models/Device.php b/app/Models/Device.php
index 8d75fb5..6a99fcd 100644
--- a/app/Models/Device.php
+++ b/app/Models/Device.php
@@ -35,7 +35,7 @@ class Device extends Model
'pause_until' => 'datetime',
];
- public function getBatteryPercentAttribute()
+ public function getBatteryPercentAttribute(): int|float
{
$volts = $this->last_battery_voltage;
@@ -83,7 +83,7 @@ class Device extends Model
return round($voltage, 2);
}
- public function getWifiStrengthAttribute()
+ public function getWifiStrengthAttribute(): int
{
$rssi = $this->last_rssi_level;
if ($rssi >= 0) {
@@ -106,11 +106,7 @@ class Device extends Model
return true;
}
- if ($this->proxy_cloud_response && $this->proxy_cloud_response['update_firmware']) {
- return true;
- }
-
- return false;
+ return $this->proxy_cloud_response && $this->proxy_cloud_response['update_firmware'];
}
public function getFirmwareUrlAttribute(): ?string
@@ -231,7 +227,7 @@ class Device extends Model
return false;
}
- $now = $now ? Carbon::instance($now) : now();
+ $now = $now instanceof DateTimeInterface ? Carbon::instance($now) : now();
// Handle overnight ranges (e.g. 22:00 to 06:00)
return $this->sleep_mode_from < $this->sleep_mode_to
@@ -245,7 +241,7 @@ class Device extends Model
return null;
}
- $now = $now ? Carbon::instance($now) : now();
+ $now = $now instanceof DateTimeInterface ? Carbon::instance($now) : now();
$from = $this->sleep_mode_from;
$to = $this->sleep_mode_to;
diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php
index d20798c..7b55a73 100644
--- a/app/Models/Playlist.php
+++ b/app/Models/Playlist.php
@@ -38,10 +38,8 @@ class Playlist extends Model
}
// Check weekday
- if ($this->weekdays !== null) {
- if (! in_array(now()->dayOfWeek, $this->weekdays)) {
- return false;
- }
+ if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) {
+ return false;
}
if ($this->active_from !== null && $this->active_until !== null) {
@@ -53,10 +51,8 @@ class Playlist extends Model
if ($now >= $this->active_from || $now <= $this->active_until) {
return true;
}
- } else {
- if ($now >= $this->active_from && $now <= $this->active_until) {
- return true;
- }
+ } elseif ($now >= $this->active_from && $now <= $this->active_until) {
+ return true;
}
return false;
diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php
index 9db5d4d..ad11f1d 100644
--- a/app/Models/PlaylistItem.php
+++ b/app/Models/PlaylistItem.php
@@ -153,9 +153,7 @@ class PlaylistItem extends Model
$plugins = Plugin::whereIn('id', $pluginIds)->get();
// Sort the collection to match plugin_ids order
- $plugins = $plugins->sortBy(function ($plugin) use ($pluginIds) {
- return array_search($plugin->id, $pluginIds);
- })->values();
+ $plugins = $plugins->sortBy(fn ($plugin): int|string|false => array_search($plugin->id, $pluginIds))->values();
foreach ($plugins as $index => $plugin) {
$size = $this->getLayoutSize($index);
diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index ab2f825..2fd3718 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -42,7 +42,7 @@ class Plugin extends Model
{
parent::boot();
- static::creating(function ($model) {
+ static::creating(function ($model): void {
if (empty($model->uuid)) {
$model->uuid = Str::uuid();
}
@@ -83,7 +83,7 @@ class Plugin extends Model
$currentValue = $this->configuration[$fieldKey] ?? null;
// If the field has a default value and no current value is set, it's not missing
- if (($currentValue === null || $currentValue === '' || (is_array($currentValue) && empty($currentValue))) && ! isset($field['default'])) {
+ if (($currentValue === null || $currentValue === '' || ($currentValue === [])) && ! isset($field['default'])) {
return true; // Found a required field that is not set and has no default
}
}
@@ -126,7 +126,7 @@ class Plugin extends Model
// Split URLs by newline and filter out empty lines
$urls = array_filter(
array_map('trim', explode("\n", $this->polling_url)),
- fn ($url) => ! empty($url)
+ fn ($url): bool => ! empty($url)
);
// If only one URL, use the original logic without nesting
@@ -237,7 +237,7 @@ class Plugin extends Model
// Converts to: {% assign temp_filtered = collection | filter: "key", "value" %}{% for item in temp_filtered %}
$template = preg_replace_callback(
'/{%\s*for\s+(\w+)\s+in\s+([^|%}]+)\s*\|\s*([^%}]+)%}/',
- function ($matches) {
+ function ($matches): string {
$variableName = mb_trim($matches[1]);
$collection = mb_trim($matches[2]);
$filter = mb_trim($matches[3]);
@@ -245,7 +245,7 @@ class Plugin extends Model
return "{% assign {$tempVarName} = {$collection} | {$filter} %}{% for {$variableName} in {$tempVarName} %}";
},
- $template
+ (string) $template
);
return $template;
diff --git a/app/Notifications/BatteryLow.php b/app/Notifications/BatteryLow.php
index c76c87f..09a5755 100644
--- a/app/Notifications/BatteryLow.php
+++ b/app/Notifications/BatteryLow.php
@@ -13,15 +13,10 @@ class BatteryLow extends Notification
{
use Queueable;
- private Device $device;
-
/**
* Create a new notification instance.
*/
- public function __construct(Device $device)
- {
- $this->device = $device;
- }
+ public function __construct(private Device $device) {}
/**
* Get the notification's delivery channels.
diff --git a/app/Notifications/Channels/WebhookChannel.php b/app/Notifications/Channels/WebhookChannel.php
index d116200..796cb24 100644
--- a/app/Notifications/Channels/WebhookChannel.php
+++ b/app/Notifications/Channels/WebhookChannel.php
@@ -11,13 +11,7 @@ use Illuminate\Support\Arr;
class WebhookChannel extends Notification
{
- /** @var Client */
- protected $client;
-
- public function __construct(Client $client)
- {
- $this->client = $client;
- }
+ public function __construct(protected Client $client) {}
/**
* Send the given notification.
diff --git a/app/Notifications/Messages/WebhookMessage.php b/app/Notifications/Messages/WebhookMessage.php
index 920c16d..6dc58eb 100644
--- a/app/Notifications/Messages/WebhookMessage.php
+++ b/app/Notifications/Messages/WebhookMessage.php
@@ -13,13 +13,6 @@ final class WebhookMessage extends Notification
*/
private $query;
- /**
- * The POST data of the Webhook request.
- *
- * @var mixed
- */
- private $data;
-
/**
* The headers to send with the request.
*
@@ -36,9 +29,8 @@ final class WebhookMessage extends Notification
/**
* @param mixed $data
- * @return static
*/
- public static function create($data = '')
+ public static function create($data = ''): self
{
return new self($data);
}
@@ -46,10 +38,12 @@ final class WebhookMessage extends Notification
/**
* @param mixed $data
*/
- public function __construct($data = '')
- {
- $this->data = $data;
- }
+ public function __construct(
+ /**
+ * The POST data of the Webhook request.
+ */
+ private $data = ''
+ ) {}
/**
* Set the Webhook parameters to be URL encoded.
@@ -57,7 +51,7 @@ final class WebhookMessage extends Notification
* @param mixed $query
* @return $this
*/
- public function query($query)
+ public function query($query): self
{
$this->query = $query;
@@ -70,7 +64,7 @@ final class WebhookMessage extends Notification
* @param mixed $data
* @return $this
*/
- public function data($data)
+ public function data($data): self
{
$this->data = $data;
@@ -84,7 +78,7 @@ final class WebhookMessage extends Notification
* @param string $value
* @return $this
*/
- public function header($name, $value)
+ public function header($name, $value): self
{
$this->headers[$name] = $value;
@@ -97,7 +91,7 @@ final class WebhookMessage extends Notification
* @param string $userAgent
* @return $this
*/
- public function userAgent($userAgent)
+ public function userAgent($userAgent): self
{
$this->headers['User-Agent'] = $userAgent;
@@ -109,17 +103,14 @@ final class WebhookMessage extends Notification
*
* @return $this
*/
- public function verify($value = true)
+ public function verify($value = true): self
{
$this->verify = $value;
return $this;
}
- /**
- * @return array
- */
- public function toArray()
+ public function toArray(): array
{
return [
'query' => $this->query,
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index b7deb3b..b8ad9bb 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -33,17 +33,19 @@ class AppServiceProvider extends ServiceProvider
$http = clone $this;
$http->server->set('HTTPS', 'off');
+ if (URL::hasValidSignature($https, $absolute, $ignoreQuery)) {
+ return true;
+ }
- return URL::hasValidSignature($https, $absolute, $ignoreQuery)
- || URL::hasValidSignature($http, $absolute, $ignoreQuery);
+ return URL::hasValidSignature($http, $absolute, $ignoreQuery);
});
// Register OIDC provider with Socialite
- Socialite::extend('oidc', function ($app) {
- $config = $app['config']['services.oidc'] ?? [];
+ Socialite::extend('oidc', function (\Illuminate\Contracts\Foundation\Application $app): OidcProvider {
+ $config = $app->make('config')->get('services.oidc', []);
return new OidcProvider(
- $app['request'],
+ $app->make(Request::class),
$config['client_id'] ?? null,
$config['client_secret'] ?? null,
$config['redirect'] ?? null,
diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php
index 762d449..36597d7 100644
--- a/app/Services/ImageGenerationService.php
+++ b/app/Services/ImageGenerationService.php
@@ -233,14 +233,14 @@ class ImageGenerationService
if ($plugin?->id) {
// Check if any devices have custom dimensions or use non-standard DeviceModels
$hasCustomDimensions = Device::query()
- ->where(function ($query) {
+ ->where(function ($query): void {
$query->where('width', '!=', 800)
->orWhere('height', '!=', 480)
->orWhere('rotate', '!=', 0);
})
- ->orWhereHas('deviceModel', function ($query) {
+ ->orWhereHas('deviceModel', function ($query): void {
// Only allow caching if all device models have standard dimensions (800x480, rotation=0)
- $query->where(function ($subQuery) {
+ $query->where(function ($subQuery): void {
$subQuery->where('width', '!=', 800)
->orWhere('height', '!=', 480)
->orWhere('rotation', '!=', 0);
diff --git a/app/Services/OidcProvider.php b/app/Services/OidcProvider.php
index 74143f1..8ea2e44 100644
--- a/app/Services/OidcProvider.php
+++ b/app/Services/OidcProvider.php
@@ -33,7 +33,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
/**
* Create a new provider instance.
*/
- public function __construct($request, $clientId, $clientSecret, $redirectUrl, $scopes = [], $guzzle = [])
+ public function __construct(\Illuminate\Http\Request $request, $clientId, $clientSecret, $redirectUrl, $scopes = [], $guzzle = [])
{
parent::__construct($request, $clientId, $clientSecret, $redirectUrl, $guzzle);
@@ -43,7 +43,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
}
// Handle both full well-known URL and base URL
- if (str_ends_with($endpoint, '/.well-known/openid-configuration')) {
+ if (str_ends_with((string) $endpoint, '/.well-known/openid-configuration')) {
$this->baseUrl = str_replace('/.well-known/openid-configuration', '', $endpoint);
} else {
$this->baseUrl = mb_rtrim($endpoint, '/');
@@ -73,7 +73,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
}
} catch (Exception $e) {
- throw new Exception('Failed to load OIDC configuration: '.$e->getMessage());
+ throw new Exception('Failed to load OIDC configuration: '.$e->getMessage(), $e->getCode(), $e);
}
}
diff --git a/app/Services/PluginExportService.php b/app/Services/PluginExportService.php
index 9f08d76..4cd246d 100644
--- a/app/Services/PluginExportService.php
+++ b/app/Services/PluginExportService.php
@@ -47,44 +47,33 @@ class PluginExportService
$tempDirName = 'temp/'.uniqid('plugin_export_', true);
Storage::makeDirectory($tempDirName);
$tempDir = Storage::path($tempDirName);
-
- try {
- // Generate settings.yml content
- $settings = $this->generateSettingsYaml($plugin);
- $settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
- File::put($tempDir.'/settings.yml', $settingsYaml);
-
- // Generate full template content
- $fullTemplate = $this->generateFullTemplate($plugin);
- $extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php';
- File::put($tempDir.'/full.'.$extension, $fullTemplate);
-
- // Generate shared.liquid if needed (for liquid templates)
- if ($plugin->markup_language === 'liquid') {
- $sharedTemplate = $this->generateSharedTemplate($plugin);
- if ($sharedTemplate) {
- File::put($tempDir.'/shared.liquid', $sharedTemplate);
- }
+ // Generate settings.yml content
+ $settings = $this->generateSettingsYaml($plugin);
+ $settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ File::put($tempDir.'/settings.yml', $settingsYaml);
+ // Generate full template content
+ $fullTemplate = $this->generateFullTemplate($plugin);
+ $extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php';
+ File::put($tempDir.'/full.'.$extension, $fullTemplate);
+ // Generate shared.liquid if needed (for liquid templates)
+ if ($plugin->markup_language === 'liquid') {
+ $sharedTemplate = $this->generateSharedTemplate();
+ if ($sharedTemplate) {
+ File::put($tempDir.'/shared.liquid', $sharedTemplate);
}
-
- // Create ZIP file
- $zipPath = $tempDir.'/plugin_'.$plugin->trmnlp_id.'.zip';
- $zip = new ZipArchive();
-
- if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
- throw new Exception('Could not create ZIP file.');
- }
-
- // Add files directly to ZIP root
- $this->addDirectoryToZip($zip, $tempDir, '');
- $zip->close();
-
- // Return the ZIP file as a download response
- return response()->download($zipPath, 'plugin_'.$plugin->trmnlp_id.'.zip');
-
- } catch (Exception $e) {
- throw $e;
}
+ // Create ZIP file
+ $zipPath = $tempDir.'/plugin_'.$plugin->trmnlp_id.'.zip';
+ $zip = new ZipArchive();
+ if ($zip->open($zipPath, ZipArchive::CREATE) !== true) {
+ throw new Exception('Could not create ZIP file.');
+ }
+ // Add files directly to ZIP root
+ $this->addDirectoryToZip($zip, $tempDir, '');
+ $zip->close();
+
+ // Return the ZIP file as a download response
+ return response()->download($zipPath, 'plugin_'.$plugin->trmnlp_id.'.zip');
}
/**
@@ -150,7 +139,7 @@ class PluginExportService
/**
* Generate the shared template content (for liquid templates)
*/
- private function generateSharedTemplate(Plugin $plugin)
+ private function generateSharedTemplate(): null
{
// For now, we don't have a way to store shared templates separately
// TODO - add support for shared templates
@@ -170,14 +159,10 @@ class PluginExportService
foreach ($files as $file) {
if (! $file->isDir()) {
$filePath = $file->getRealPath();
- $fileName = basename($filePath);
+ $fileName = basename((string) $filePath);
// For root directory, just use the filename
- if ($zipPath === '') {
- $relativePath = $fileName;
- } else {
- $relativePath = $zipPath.'/'.mb_substr($filePath, mb_strlen($dirPath) + 1);
- }
+ $relativePath = $zipPath === '' ? $fileName : $zipPath.'/'.mb_substr((string) $filePath, mb_strlen($dirPath) + 1);
$zip->addFile($filePath, $relativePath);
}
diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php
index e824f35..5cc928b 100644
--- a/app/Services/PluginImportService.php
+++ b/app/Services/PluginImportService.php
@@ -72,7 +72,7 @@ class PluginImportService
// Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
- if (pathinfo($filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
+ if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
}
@@ -197,7 +197,7 @@ class PluginImportService
// Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
- if (pathinfo($filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
+ if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
}
@@ -352,7 +352,7 @@ class PluginImportService
// check if they're in the root of the ZIP or in a subfolder
if ($settingsYamlPath && $fullLiquidPath) {
// If the files are in the root of the ZIP, create a src folder and move them there
- $srcDir = dirname($settingsYamlPath);
+ $srcDir = dirname((string) $settingsYamlPath);
// If the parent directory is not named 'src', create a src directory
if (basename($srcDir) !== 'src') {
diff --git a/composer.json b/composer.json
index eee7896..8f3079d 100644
--- a/composer.json
+++ b/composer.json
@@ -37,7 +37,8 @@
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-drift": "^4.0",
- "pestphp/pest-plugin-laravel": "^4.0"
+ "pestphp/pest-plugin-laravel": "^4.0",
+ "rector/rector": "^2.1"
},
"autoload": {
"psr-4": {
@@ -75,7 +76,8 @@
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint",
"analyse": "vendor/bin/phpstan analyse",
- "analyze": "vendor/bin/phpstan analyse"
+ "analyze": "vendor/bin/phpstan analyse",
+ "rector": "vendor/bin/rector process"
},
"extra": {
"laravel": {
diff --git a/composer.lock b/composer.lock
index cf2ce06..09facfa 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7d12a2e6d66b2e82c6d96d6a0c5366f0",
+ "content-hash": "9122624c0df3b24bc94c7c866aa4e17c",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -10546,6 +10546,66 @@
],
"time": "2025-09-03T06:25:17+00:00"
},
+ {
+ "name": "rector/rector",
+ "version": "2.1.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rectorphp/rector.git",
+ "reference": "c34cc07c4698f007a20dc5c99ff820089ae413ce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/c34cc07c4698f007a20dc5c99ff820089ae413ce",
+ "reference": "c34cc07c4698f007a20dc5c99ff820089ae413ce",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0",
+ "phpstan/phpstan": "^2.1.18"
+ },
+ "conflict": {
+ "rector/rector-doctrine": "*",
+ "rector/rector-downgrade-php": "*",
+ "rector/rector-phpunit": "*",
+ "rector/rector-symfony": "*"
+ },
+ "suggest": {
+ "ext-dom": "To manipulate phpunit.xml via the custom-rule command"
+ },
+ "bin": [
+ "bin/rector"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Instant Upgrade and Automated Refactoring of any PHP code",
+ "homepage": "https://getrector.com/",
+ "keywords": [
+ "automation",
+ "dev",
+ "migration",
+ "refactoring"
+ ],
+ "support": {
+ "issues": "https://github.com/rectorphp/rector/issues",
+ "source": "https://github.com/rectorphp/rector/tree/2.1.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/tomasvotruba",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-10T11:13:58+00:00"
+ },
{
"name": "sebastian/cli-parser",
"version": "4.2.0",
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..dde2f14
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,26 @@
+paths([
+ __DIR__.'/app',
+ __DIR__.'/tests',
+ ]);
+
+ $rectorConfig->sets([
+ LevelSetList::UP_TO_PHP_82,
+ SetList::CODE_QUALITY,
+ SetList::DEAD_CODE,
+ SetList::EARLY_RETURN,
+ SetList::TYPE_DECLARATION,
+ ]);
+
+ $rectorConfig->skip([
+ // Skip any specific rules if needed
+ ]);
+};
diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php
index b72280a..005e73e 100644
--- a/tests/Feature/Api/DeviceEndpointsTest.php
+++ b/tests/Feature/Api/DeviceEndpointsTest.php
@@ -14,13 +14,13 @@ use Laravel\Sanctum\Sanctum;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-beforeEach(function () {
+beforeEach(function (): void {
TrmnlPipeline::fake();
Storage::fake('public');
Storage::disk('public')->makeDirectory('/images/generated');
});
-test('device can fetch display data with valid credentials', function () {
+test('device can fetch display data with valid credentials', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -52,7 +52,7 @@ test('device can fetch display data with valid credentials', function () {
->last_firmware_version->toBe('1.0.0');
});
-test('display endpoint includes image_url_timeout when configured', function () {
+test('display endpoint includes image_url_timeout when configured', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -74,7 +74,7 @@ test('display endpoint includes image_url_timeout when configured', function ()
]);
});
-test('display endpoint omits image_url_timeout when not configured', function () {
+test('display endpoint omits image_url_timeout when not configured', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -94,7 +94,7 @@ test('display endpoint omits image_url_timeout when not configured', function ()
->assertJsonMissing(['image_url_timeout']);
});
-test('new device is auto-assigned to user with auto-assign enabled', function () {
+test('new device is auto-assigned to user with auto-assign enabled', function (): void {
$user = User::factory()->create(['assign_new_devices' => true]);
$response = $this->withHeaders([
@@ -114,7 +114,7 @@ test('new device is auto-assigned to user with auto-assign enabled', function ()
->api_key->toBe('new-device-key');
});
-test('new device is auto-assigned and mirrors specified device', function () {
+test('new device is auto-assigned and mirrors specified device', function (): void {
// Create a source device that will be mirrored
$sourceDevice = Device::factory()->create([
'mac_address' => 'AA:BB:CC:DD:EE:FF',
@@ -153,7 +153,7 @@ test('new device is auto-assigned and mirrors specified device', function () {
]);
});
-test('device setup endpoint returns correct data', function () {
+test('device setup endpoint returns correct data', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -172,7 +172,7 @@ test('device setup endpoint returns correct data', function () {
]);
});
-test('device can submit logs', function () {
+test('device can submit logs', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -200,7 +200,7 @@ test('device can submit logs', function () {
expect($device->logs()->count())->toBe(1);
});
-test('device can submit logs in revised format', function () {
+test('device can submit logs in revised format', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -240,7 +240,7 @@ test('device can submit logs in revised format', function () {
// $response->assertOk();
// });
-test('user cannot update display for devices they do not own', function () {
+test('user cannot update display for devices they do not own', function (): void {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$device = Device::factory()->create(['user_id' => $otherUser->id]);
@@ -255,7 +255,7 @@ test('user cannot update display for devices they do not own', function () {
$response->assertForbidden();
});
-test('invalid device credentials return error', function () {
+test('invalid device credentials return error', function (): void {
$response = $this->withHeaders([
'id' => 'invalid-mac',
'access-token' => 'invalid-token',
@@ -265,7 +265,7 @@ test('invalid device credentials return error', function () {
->assertJson(['message' => 'MAC Address not registered or invalid access token']);
});
-test('log endpoint requires valid device credentials', function () {
+test('log endpoint requires valid device credentials', function (): void {
$response = $this->withHeaders([
'id' => 'invalid-mac',
'access-token' => 'invalid-token',
@@ -275,7 +275,7 @@ test('log endpoint requires valid device credentials', function () {
->assertJson(['message' => 'Device not found or invalid access token']);
});
-test('update_firmware flag is only returned once', function () {
+test('update_firmware flag is only returned once', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -320,7 +320,7 @@ test('update_firmware flag is only returned once', function () {
expect($device->proxy_cloud_response['update_firmware'])->toBeFalse();
});
-test('authenticated user can fetch device status', function () {
+test('authenticated user can fetch device status', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create([
'user_id' => $user->id,
@@ -354,7 +354,7 @@ test('authenticated user can fetch device status', function () {
]);
});
-test('user cannot fetch status for devices they do not own', function () {
+test('user cannot fetch status for devices they do not own', function (): void {
$user = User::factory()->create();
$otherUser = User::factory()->create();
$device = Device::factory()->create(['user_id' => $otherUser->id]);
@@ -366,7 +366,7 @@ test('user cannot fetch status for devices they do not own', function () {
$response->assertForbidden();
});
-test('display status endpoint requires device_id parameter', function () {
+test('display status endpoint requires device_id parameter', function (): void {
$user = User::factory()->create();
Sanctum::actingAs($user);
@@ -376,7 +376,7 @@ test('display status endpoint requires device_id parameter', function () {
->assertJsonValidationErrors(['device_id']);
});
-test('display status endpoint requires valid device_id', function () {
+test('display status endpoint requires valid device_id', function (): void {
$user = User::factory()->create();
Sanctum::actingAs($user);
@@ -386,7 +386,7 @@ test('display status endpoint requires valid device_id', function () {
->assertJsonValidationErrors(['device_id']);
});
-test('device can mirror another device', function () {
+test('device can mirror another device', function (): void {
// Create source device with a playlist and image
$sourceDevice = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
@@ -428,7 +428,7 @@ test('device can mirror another device', function () {
->last_firmware_version->toBe('1.0.0');
});
-test('device can fetch current screen data', function () {
+test('device can fetch current screen data', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -451,7 +451,7 @@ test('device can fetch current screen data', function () {
]);
});
-test('current_screen endpoint requires valid device credentials', function () {
+test('current_screen endpoint requires valid device credentials', function (): void {
$response = $this->withHeaders([
'access-token' => 'invalid-token',
])->get('/api/current_screen');
@@ -460,7 +460,7 @@ test('current_screen endpoint requires valid device credentials', function () {
->assertJson(['message' => 'Device not found or invalid access token']);
});
-test('authenticated user can fetch their devices', function () {
+test('authenticated user can fetch their devices', function (): void {
$user = User::factory()->create();
$devices = Device::factory()->count(2)->create([
'user_id' => $user->id,
@@ -502,7 +502,7 @@ test('authenticated user can fetch their devices', function () {
]);
});
-test('plugin caches image until data is stale', function () {
+test('plugin caches image until data is stale', function (): void {
// Create source device with a playlist
$device = Device::factory()->create([
'mac_address' => '55:11:22:33:44:55',
@@ -577,7 +577,7 @@ test('plugin caches image until data is stale', function () {
->not->toBe($firstResponse['filename']);
});
-test('plugins in playlist are rendered in order', function () {
+test('plugins in playlist are rendered in order', function (): void {
// Create source device with a playlist
$device = Device::factory()->create([
'mac_address' => '55:11:22:33:44:55',
@@ -681,7 +681,7 @@ test('plugins in playlist are rendered in order', function () {
->not->toBe($secondResponse['filename']);
});
-test('display endpoint updates last_refreshed_at timestamp', function () {
+test('display endpoint updates last_refreshed_at timestamp', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -702,7 +702,7 @@ test('display endpoint updates last_refreshed_at timestamp', function () {
->and($device->last_refreshed_at->diffInSeconds(now()))->toBeLessThan(2);
});
-test('display endpoint updates last_refreshed_at timestamp for mirrored devices', function () {
+test('display endpoint updates last_refreshed_at timestamp for mirrored devices', function (): void {
// Create source device
$sourceDevice = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
@@ -731,7 +731,7 @@ test('display endpoint updates last_refreshed_at timestamp for mirrored devices'
->and($mirrorDevice->last_refreshed_at->diffInSeconds(now()))->toBeLessThan(2);
});
-test('display endpoint handles mashup playlist items correctly', function () {
+test('display endpoint handles mashup playlist items correctly', function (): void {
// Create a device
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
@@ -791,7 +791,7 @@ test('display endpoint handles mashup playlist items correctly', function () {
expect($playlistItem->last_displayed_at)->not->toBeNull();
});
-test('device in sleep mode returns sleep image and correct refresh rate', function () {
+test('device in sleep mode returns sleep image and correct refresh rate', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -821,7 +821,7 @@ test('device in sleep mode returns sleep image and correct refresh rate', functi
Carbon\Carbon::setTestNow(); // Clear test time
});
-test('device not in sleep mode returns normal image', function () {
+test('device not in sleep mode returns normal image', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -850,7 +850,7 @@ test('device not in sleep mode returns normal image', function () {
Carbon\Carbon::setTestNow(); // Clear test time
});
-test('device returns sleep.png and correct refresh time when paused', function () {
+test('device returns sleep.png and correct refresh time when paused', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -872,7 +872,7 @@ test('device returns sleep.png and correct refresh time when paused', function (
expect($json['refresh_rate'])->toBeLessThanOrEqual(3600); // ~60 min
});
-test('screens endpoint accepts nullable file_name', function () {
+test('screens endpoint accepts nullable file_name', function (): void {
Queue::fake();
$device = Device::factory()->create([
@@ -894,7 +894,7 @@ test('screens endpoint accepts nullable file_name', function () {
Queue::assertPushed(GenerateScreenJob::class);
});
-test('screens endpoint returns 404 for invalid device credentials', function () {
+test('screens endpoint returns 404 for invalid device credentials', function (): void {
$response = $this->withHeaders([
'id' => 'invalid-mac',
'access-token' => 'invalid-key',
@@ -911,7 +911,7 @@ test('screens endpoint returns 404 for invalid device credentials', function ()
]);
});
-test('setup endpoint assigns device model when model-id header is provided', function () {
+test('setup endpoint assigns device model when model-id header is provided', function (): void {
$user = User::factory()->create(['assign_new_devices' => true]);
$deviceModel = DeviceModel::factory()->create([
'name' => 'test-model',
@@ -934,7 +934,7 @@ test('setup endpoint assigns device model when model-id header is provided', fun
->and($device->device_model_id)->toBe($deviceModel->id);
});
-test('setup endpoint handles non-existent device model gracefully', function () {
+test('setup endpoint handles non-existent device model gracefully', function (): void {
$user = User::factory()->create(['assign_new_devices' => true]);
$response = $this->withHeaders([
diff --git a/tests/Feature/Api/DeviceImageFormatTest.php b/tests/Feature/Api/DeviceImageFormatTest.php
index fcb7555..a7db928 100644
--- a/tests/Feature/Api/DeviceImageFormatTest.php
+++ b/tests/Feature/Api/DeviceImageFormatTest.php
@@ -10,12 +10,12 @@ use Illuminate\Support\Facades\Storage;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-beforeEach(function () {
+beforeEach(function (): void {
Storage::fake('public');
Storage::disk('public')->makeDirectory('/images/generated');
});
-test('device with firmware version 1.5.1 gets bmp format', function () {
+test('device with firmware version 1.5.1 gets bmp format', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -52,7 +52,7 @@ test('device with firmware version 1.5.1 gets bmp format', function () {
]);
});
-test('device with firmware version 1.5.2 gets png format', function () {
+test('device with firmware version 1.5.2 gets png format', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -88,7 +88,7 @@ test('device with firmware version 1.5.2 gets png format', function () {
]);
});
-test('device falls back to bmp when png does not exist', function () {
+test('device falls back to bmp when png does not exist', function (): void {
$device = Device::factory()->create([
'mac_address' => '00:11:22:33:44:55',
'api_key' => 'test-api-key',
@@ -124,7 +124,7 @@ test('device falls back to bmp when png does not exist', function () {
]);
});
-test('device without device_model_id and image_format bmp3_1bit_srgb returns bmp when plugin is rendered', function () {
+test('device without device_model_id and image_format bmp3_1bit_srgb returns bmp when plugin is rendered', function (): void {
// Create a user with auto-assign enabled
$user = User::factory()->create([
'assign_new_devices' => true,
diff --git a/tests/Feature/Api/PluginSettingsArchiveTest.php b/tests/Feature/Api/PluginSettingsArchiveTest.php
index 517f2f8..f0ad3d0 100644
--- a/tests/Feature/Api/PluginSettingsArchiveTest.php
+++ b/tests/Feature/Api/PluginSettingsArchiveTest.php
@@ -8,7 +8,7 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;
-it('accepts a plugin settings archive and updates the plugin', function () {
+it('accepts a plugin settings archive and updates the plugin', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php
index 96edffc..07c1683 100644
--- a/tests/Feature/Auth/AuthenticationTest.php
+++ b/tests/Feature/Auth/AuthenticationTest.php
@@ -5,13 +5,13 @@ use Livewire\Volt\Volt as LivewireVolt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('login screen can be rendered', function () {
+test('login screen can be rendered', function (): void {
$response = $this->get('/login');
$response->assertStatus(200);
});
-test('users can authenticate using the login screen', function () {
+test('users can authenticate using the login screen', function (): void {
$user = User::factory()->create();
$response = LivewireVolt::test('auth.login')
@@ -26,7 +26,7 @@ test('users can authenticate using the login screen', function () {
$this->assertAuthenticated();
});
-test('users can not authenticate with invalid password', function () {
+test('users can not authenticate with invalid password', function (): void {
$user = User::factory()->create();
$this->post('/login', [
@@ -37,7 +37,7 @@ test('users can not authenticate with invalid password', function () {
$this->assertGuest();
});
-test('users can logout', function () {
+test('users can logout', function (): void {
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/logout');
diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php
index 52a663d..5cc2db8 100644
--- a/tests/Feature/Auth/EmailVerificationTest.php
+++ b/tests/Feature/Auth/EmailVerificationTest.php
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\URL;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('email verification screen can be rendered', function () {
+test('email verification screen can be rendered', function (): void {
$user = User::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/verify-email');
@@ -15,7 +15,7 @@ test('email verification screen can be rendered', function () {
$response->assertStatus(200);
});
-test('email can be verified', function () {
+test('email can be verified', function (): void {
$user = User::factory()->unverified()->create();
Event::fake();
@@ -23,7 +23,7 @@ test('email can be verified', function () {
$verificationUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes(60),
- ['id' => $user->id, 'hash' => sha1($user->email)]
+ ['id' => $user->id, 'hash' => sha1((string) $user->email)]
);
$response = $this->actingAs($user)->get($verificationUrl);
@@ -34,7 +34,7 @@ test('email can be verified', function () {
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
});
-test('email is not verified with invalid hash', function () {
+test('email is not verified with invalid hash', function (): void {
$user = User::factory()->unverified()->create();
$verificationUrl = URL::temporarySignedRoute(
diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php
index efb11ce..265963a 100644
--- a/tests/Feature/Auth/PasswordConfirmationTest.php
+++ b/tests/Feature/Auth/PasswordConfirmationTest.php
@@ -5,7 +5,7 @@ use Livewire\Volt\Volt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('confirm password screen can be rendered', function () {
+test('confirm password screen can be rendered', function (): void {
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/confirm-password');
@@ -13,7 +13,7 @@ test('confirm password screen can be rendered', function () {
$response->assertStatus(200);
});
-test('password can be confirmed', function () {
+test('password can be confirmed', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -27,7 +27,7 @@ test('password can be confirmed', function () {
->assertRedirect(route('dashboard', absolute: false));
});
-test('password is not confirmed with invalid password', function () {
+test('password is not confirmed with invalid password', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php
index 86fda9d..2f38263 100644
--- a/tests/Feature/Auth/PasswordResetTest.php
+++ b/tests/Feature/Auth/PasswordResetTest.php
@@ -7,13 +7,13 @@ use Livewire\Volt\Volt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('reset password link screen can be rendered', function () {
+test('reset password link screen can be rendered', function (): void {
$response = $this->get('/forgot-password');
$response->assertStatus(200);
});
-test('reset password link can be requested', function () {
+test('reset password link can be requested', function (): void {
Notification::fake();
$user = User::factory()->create();
@@ -25,7 +25,7 @@ test('reset password link can be requested', function () {
Notification::assertSentTo($user, ResetPassword::class);
});
-test('reset password screen can be rendered', function () {
+test('reset password screen can be rendered', function (): void {
Notification::fake();
$user = User::factory()->create();
@@ -34,7 +34,7 @@ test('reset password screen can be rendered', function () {
->set('email', $user->email)
->call('sendPasswordResetLink');
- Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification): true {
$response = $this->get('/reset-password/'.$notification->token);
$response->assertStatus(200);
@@ -43,7 +43,7 @@ test('reset password screen can be rendered', function () {
});
});
-test('password can be reset with valid token', function () {
+test('password can be reset with valid token', function (): void {
Notification::fake();
$user = User::factory()->create();
@@ -52,7 +52,7 @@ test('password can be reset with valid token', function () {
->set('email', $user->email)
->call('sendPasswordResetLink');
- Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user): true {
$response = Volt::test('auth.reset-password', ['token' => $notification->token])
->set('email', $user->email)
->set('password', 'password')
diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php
index a1c4c07..45bc39b 100644
--- a/tests/Feature/Auth/RegistrationTest.php
+++ b/tests/Feature/Auth/RegistrationTest.php
@@ -4,13 +4,13 @@ use Livewire\Volt\Volt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('registration screen can be rendered', function () {
+test('registration screen can be rendered', function (): void {
$response = $this->get('/register');
$response->assertStatus(200);
});
-test('new users can register', function () {
+test('new users can register', function (): void {
$response = Volt::test('auth.register')
->set('name', 'Test User')
->set('email', 'test@example.com')
diff --git a/tests/Feature/Console/ExampleRecipesSeederCommandTest.php b/tests/Feature/Console/ExampleRecipesSeederCommandTest.php
index 4b98180..74241e0 100644
--- a/tests/Feature/Console/ExampleRecipesSeederCommandTest.php
+++ b/tests/Feature/Console/ExampleRecipesSeederCommandTest.php
@@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('example recipes seeder command calls seeder with correct user id', function () {
+test('example recipes seeder command calls seeder with correct user id', function (): void {
$seeder = Mockery::mock(ExampleRecipesSeeder::class);
$seeder->shouldReceive('run')
->once()
@@ -19,14 +19,14 @@ test('example recipes seeder command calls seeder with correct user id', functio
->assertExitCode(0);
});
-test('example recipes seeder command has correct signature', function () {
+test('example recipes seeder command has correct signature', function (): void {
$command = $this->app->make(App\Console\Commands\ExampleRecipesSeederCommand::class);
expect($command->getName())->toBe('recipes:seed');
expect($command->getDescription())->toBe('Seed example recipes');
});
-test('example recipes seeder command prompts for missing input', function () {
+test('example recipes seeder command prompts for missing input', function (): void {
$seeder = Mockery::mock(ExampleRecipesSeeder::class);
$seeder->shouldReceive('run')
->once()
diff --git a/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php b/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php
index b34357d..e8d12f0 100644
--- a/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php
+++ b/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php
@@ -3,7 +3,7 @@
use App\Jobs\FetchProxyCloudResponses;
use Illuminate\Support\Facades\Bus;
-test('it dispatches fetch proxy cloud responses job', function () {
+test('it dispatches fetch proxy cloud responses job', function (): void {
// Prevent the job from actually running
Bus::fake();
diff --git a/tests/Feature/Console/FirmwareCheckCommandTest.php b/tests/Feature/Console/FirmwareCheckCommandTest.php
index 19098ea..e0ed205 100644
--- a/tests/Feature/Console/FirmwareCheckCommandTest.php
+++ b/tests/Feature/Console/FirmwareCheckCommandTest.php
@@ -6,24 +6,24 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('firmware check command has correct signature', function () {
+test('firmware check command has correct signature', function (): void {
$command = $this->app->make(App\Console\Commands\FirmwareCheckCommand::class);
expect($command->getName())->toBe('trmnl:firmware:check');
expect($command->getDescription())->toBe('Checks for the latest firmware and downloads it if flag --download is passed.');
});
-test('firmware check command runs without errors', function () {
+test('firmware check command runs without errors', function (): void {
$this->artisan('trmnl:firmware:check')
->assertExitCode(0);
});
-test('firmware check command runs with download flag', function () {
+test('firmware check command runs with download flag', function (): void {
$this->artisan('trmnl:firmware:check', ['--download' => true])
->assertExitCode(0);
});
-test('firmware check command can run successfully', function () {
+test('firmware check command can run successfully', function (): void {
$this->artisan('trmnl:firmware:check')
->assertExitCode(0);
});
diff --git a/tests/Feature/Console/FirmwareUpdateCommandTest.php b/tests/Feature/Console/FirmwareUpdateCommandTest.php
index ee250b9..3e8c916 100644
--- a/tests/Feature/Console/FirmwareUpdateCommandTest.php
+++ b/tests/Feature/Console/FirmwareUpdateCommandTest.php
@@ -6,12 +6,12 @@ use App\Models\Device;
use App\Models\Firmware;
use App\Models\User;
-test('firmware update command has correct signature', function () {
+test('firmware update command has correct signature', function (): void {
$this->artisan('trmnl:firmware:update --help')
->assertExitCode(0);
});
-test('firmware update command can be called', function () {
+test('firmware update command can be called', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
@@ -26,7 +26,7 @@ test('firmware update command can be called', function () {
expect($device->update_firmware_id)->toBe($firmware->id);
});
-test('firmware update command updates all devices when all is selected', function () {
+test('firmware update command updates all devices when all is selected', function (): void {
$user = User::factory()->create();
$device1 = Device::factory()->create(['user_id' => $user->id]);
$device2 = Device::factory()->create(['user_id' => $user->id]);
@@ -44,7 +44,7 @@ test('firmware update command updates all devices when all is selected', functio
expect($device2->update_firmware_id)->toBe($firmware->id);
});
-test('firmware update command aborts when no devices selected', function () {
+test('firmware update command aborts when no devices selected', function (): void {
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
$this->artisan('trmnl:firmware:update')
@@ -55,7 +55,7 @@ test('firmware update command aborts when no devices selected', function () {
->assertExitCode(0);
});
-test('firmware update command calls firmware check when check is selected', function () {
+test('firmware update command calls firmware check when check is selected', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
@@ -70,7 +70,7 @@ test('firmware update command calls firmware check when check is selected', func
expect($device->update_firmware_id)->toBe($firmware->id);
});
-test('firmware update command calls firmware check with download when download is selected', function () {
+test('firmware update command calls firmware check with download when download is selected', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
diff --git a/tests/Feature/Console/MashupCreateCommandTest.php b/tests/Feature/Console/MashupCreateCommandTest.php
index e61c34c..e2d35eb 100644
--- a/tests/Feature/Console/MashupCreateCommandTest.php
+++ b/tests/Feature/Console/MashupCreateCommandTest.php
@@ -8,12 +8,12 @@ use App\Models\PlaylistItem;
use App\Models\Plugin;
use App\Models\User;
-test('mashup create command has correct signature', function () {
+test('mashup create command has correct signature', function (): void {
$this->artisan('mashup:create --help')
->assertExitCode(0);
});
-test('mashup create command creates mashup successfully', function () {
+test('mashup create command creates mashup successfully', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
@@ -40,13 +40,13 @@ test('mashup create command creates mashup successfully', function () {
expect($playlistItem->getMashupPluginIds())->toContain($plugin1->id, $plugin2->id);
});
-test('mashup create command exits when no devices found', function () {
+test('mashup create command exits when no devices found', function (): void {
$this->artisan('mashup:create')
->expectsOutput('No devices found. Please create a device first.')
->assertExitCode(1);
});
-test('mashup create command exits when no playlists found for device', function () {
+test('mashup create command exits when no playlists found for device', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
@@ -56,7 +56,7 @@ test('mashup create command exits when no playlists found for device', function
->assertExitCode(1);
});
-test('mashup create command exits when no plugins found', function () {
+test('mashup create command exits when no plugins found', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
@@ -70,7 +70,7 @@ test('mashup create command exits when no plugins found', function () {
->assertExitCode(1);
});
-test('mashup create command validates mashup name length', function () {
+test('mashup create command validates mashup name length', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
@@ -86,7 +86,7 @@ test('mashup create command validates mashup name length', function () {
->assertExitCode(1);
});
-test('mashup create command validates mashup name maximum length', function () {
+test('mashup create command validates mashup name maximum length', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
@@ -104,7 +104,7 @@ test('mashup create command validates mashup name maximum length', function () {
->assertExitCode(1);
});
-test('mashup create command uses default name when provided', function () {
+test('mashup create command uses default name when provided', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
@@ -128,7 +128,7 @@ test('mashup create command uses default name when provided', function () {
expect($playlistItem)->not->toBeNull();
});
-test('mashup create command handles 1x1 layout with single plugin', function () {
+test('mashup create command handles 1x1 layout with single plugin', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
diff --git a/tests/Feature/Console/OidcTestCommandTest.php b/tests/Feature/Console/OidcTestCommandTest.php
index b523574..56ccea8 100644
--- a/tests/Feature/Console/OidcTestCommandTest.php
+++ b/tests/Feature/Console/OidcTestCommandTest.php
@@ -4,12 +4,12 @@ declare(strict_types=1);
use function Pest\Laravel\mock;
-test('oidc test command has correct signature', function () {
+test('oidc test command has correct signature', function (): void {
$this->artisan('oidc:test --help')
->assertExitCode(0);
});
-test('oidc test command runs successfully with disabled oidc', function () {
+test('oidc test command runs successfully with disabled oidc', function (): void {
config([
'app.url' => 'http://localhost',
'services.oidc.enabled' => false,
@@ -40,7 +40,7 @@ test('oidc test command runs successfully with disabled oidc', function () {
->assertExitCode(0);
});
-test('oidc test command runs successfully with enabled oidc but missing config', function () {
+test('oidc test command runs successfully with enabled oidc but missing config', function (): void {
config([
'app.url' => 'http://localhost',
'services.oidc.enabled' => true,
@@ -70,7 +70,7 @@ test('oidc test command runs successfully with enabled oidc but missing config',
->assertExitCode(0);
});
-test('oidc test command runs successfully with partial config', function () {
+test('oidc test command runs successfully with partial config', function (): void {
config([
'services.oidc.enabled' => true,
'services.oidc.endpoint' => 'https://example.com',
@@ -95,9 +95,9 @@ test('oidc test command runs successfully with partial config', function () {
->assertExitCode(0);
});
-test('oidc test command runs successfully with full config but disabled', function () {
+test('oidc test command runs successfully with full config but disabled', function (): void {
// Mock the HTTP client to return fake OIDC configuration
- mock(GuzzleHttp\Client::class, function ($mock) {
+ mock(GuzzleHttp\Client::class, function ($mock): void {
$mock->shouldReceive('get')
->with('https://example.com/.well-known/openid-configuration')
->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
@@ -129,9 +129,9 @@ test('oidc test command runs successfully with full config but disabled', functi
->assertExitCode(0);
});
-test('oidc test command runs successfully with full config and enabled', function () {
+test('oidc test command runs successfully with full config and enabled', function (): void {
// Mock the HTTP client to return fake OIDC configuration
- mock(GuzzleHttp\Client::class, function ($mock) {
+ mock(GuzzleHttp\Client::class, function ($mock): void {
$mock->shouldReceive('get')
->with('https://example.com/.well-known/openid-configuration')
->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
@@ -164,9 +164,9 @@ test('oidc test command runs successfully with full config and enabled', functio
->assertExitCode(0);
});
-test('oidc test command handles empty scopes', function () {
+test('oidc test command handles empty scopes', function (): void {
// Mock the HTTP client to return fake OIDC configuration
- mock(GuzzleHttp\Client::class, function ($mock) {
+ mock(GuzzleHttp\Client::class, function ($mock): void {
$mock->shouldReceive('get')
->with('https://example.com/.well-known/openid-configuration')
->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
diff --git a/tests/Feature/Console/ScreenGeneratorCommandTest.php b/tests/Feature/Console/ScreenGeneratorCommandTest.php
index 54621d6..1f18107 100644
--- a/tests/Feature/Console/ScreenGeneratorCommandTest.php
+++ b/tests/Feature/Console/ScreenGeneratorCommandTest.php
@@ -3,7 +3,7 @@
use App\Jobs\GenerateScreenJob;
use Illuminate\Support\Facades\Bus;
-test('it generates screen with default parameters', function () {
+test('it generates screen with default parameters', function (): void {
Bus::fake();
$this->artisan('trmnl:screen:generate')
diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php
index 4ed5100..110adc8 100644
--- a/tests/Feature/DashboardTest.php
+++ b/tests/Feature/DashboardTest.php
@@ -4,12 +4,12 @@ use App\Models\User;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('guests are redirected to the login page', function () {
+test('guests are redirected to the login page', function (): void {
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
});
-test('authenticated users can visit the dashboard', function () {
+test('authenticated users can visit the dashboard', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
diff --git a/tests/Feature/Devices/DeviceConfigureTest.php b/tests/Feature/Devices/DeviceConfigureTest.php
index 85b1fd3..dff0954 100644
--- a/tests/Feature/Devices/DeviceConfigureTest.php
+++ b/tests/Feature/Devices/DeviceConfigureTest.php
@@ -10,7 +10,7 @@ use function Pest\Laravel\actingAs;
uses(RefreshDatabase::class);
-test('configure view displays last_refreshed_at timestamp', function () {
+test('configure view displays last_refreshed_at timestamp', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create([
'user_id' => $user->id,
diff --git a/tests/Feature/Devices/DeviceRotationTest.php b/tests/Feature/Devices/DeviceRotationTest.php
index e6fb7e0..35367ba 100644
--- a/tests/Feature/Devices/DeviceRotationTest.php
+++ b/tests/Feature/Devices/DeviceRotationTest.php
@@ -8,7 +8,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('dashboard shows device image with correct rotation', function () {
+test('dashboard shows device image with correct rotation', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create([
'user_id' => $user->id,
@@ -28,7 +28,7 @@ test('dashboard shows device image with correct rotation', function () {
$response->assertSee('origin-center');
});
-test('device configure page shows device image with correct rotation', function () {
+test('device configure page shows device image with correct rotation', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create([
'user_id' => $user->id,
@@ -48,7 +48,7 @@ test('device configure page shows device image with correct rotation', function
$response->assertSee('origin-center');
});
-test('device with no rotation shows no transform style', function () {
+test('device with no rotation shows no transform style', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create([
'user_id' => $user->id,
@@ -67,7 +67,7 @@ test('device with no rotation shows no transform style', function () {
$response->assertSee('-rotate-[0deg]');
});
-test('device with null rotation defaults to 0', function () {
+test('device with null rotation defaults to 0', function (): void {
$user = User::factory()->create();
$device = Device::factory()->create([
'user_id' => $user->id,
diff --git a/tests/Feature/Devices/DeviceTest.php b/tests/Feature/Devices/DeviceTest.php
index e03a82a..3cff76b 100644
--- a/tests/Feature/Devices/DeviceTest.php
+++ b/tests/Feature/Devices/DeviceTest.php
@@ -5,7 +5,7 @@ use Illuminate\Support\Carbon;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('device can be created with basic attributes', function () {
+test('device can be created with basic attributes', function (): void {
$device = Device::factory()->create([
'name' => 'Test Device',
]);
@@ -14,7 +14,7 @@ test('device can be created with basic attributes', function () {
->and($device->name)->toBe('Test Device');
});
-test('battery percentage is calculated correctly', function () {
+test('battery percentage is calculated correctly', function (): void {
$cases = [
['voltage' => 3.0, 'expected' => 0], // Min voltage
['voltage' => 4.2, 'expected' => 100], // Max voltage
@@ -34,7 +34,7 @@ test('battery percentage is calculated correctly', function () {
}
});
-test('wifi strength is determined correctly', function () {
+test('wifi strength is determined correctly', function (): void {
$cases = [
['rssi' => 0, 'expected' => 0], // No signal
['rssi' => -90, 'expected' => 1], // Weak signal
@@ -52,7 +52,7 @@ test('wifi strength is determined correctly', function () {
}
});
-test('proxy cloud attribute is properly cast to boolean', function () {
+test('proxy cloud attribute is properly cast to boolean', function (): void {
$device = Device::factory()->create([
'proxy_cloud' => true,
]);
@@ -63,7 +63,7 @@ test('proxy cloud attribute is properly cast to boolean', function () {
expect($device->proxy_cloud)->toBeFalse();
});
-test('last log request is properly cast to json', function () {
+test('last log request is properly cast to json', function (): void {
$logData = ['status' => 'success', 'timestamp' => '2024-03-04 12:00:00'];
$device = Device::factory()->create([
@@ -76,7 +76,7 @@ test('last log request is properly cast to json', function () {
->toHaveKey('timestamp');
});
-test('getSleepModeEndsInSeconds returns correct value for overnight sleep window', function () {
+test('getSleepModeEndsInSeconds returns correct value for overnight sleep window', function (): void {
// Set the current time to 12:13
Carbon::setTestNow(Carbon::create(2024, 1, 1, 12, 13, 0));
diff --git a/tests/Feature/Devices/ManageTest.php b/tests/Feature/Devices/ManageTest.php
index a629cfe..fbfd2f2 100644
--- a/tests/Feature/Devices/ManageTest.php
+++ b/tests/Feature/Devices/ManageTest.php
@@ -6,7 +6,7 @@ use Livewire\Volt\Volt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('device management page can be rendered', function () {
+test('device management page can be rendered', function (): void {
$user = User::factory()->create();
$response = $this->actingAs($user)
@@ -15,7 +15,7 @@ test('device management page can be rendered', function () {
$response->assertOk();
});
-test('user can create a new device', function () {
+test('user can create a new device', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -48,7 +48,7 @@ test('user can create a new device', function () {
expect($device->user_id)->toBe($user->id);
});
-test('device creation requires required fields', function () {
+test('device creation requires required fields', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -67,7 +67,7 @@ test('device creation requires required fields', function () {
]);
});
-test('user can toggle proxy cloud for their device', function () {
+test('user can toggle proxy cloud for their device', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
$device = Device::factory()->create([
@@ -88,7 +88,7 @@ test('user can toggle proxy cloud for their device', function () {
expect($device->fresh()->proxy_cloud)->toBeFalse();
});
-test('user cannot toggle proxy cloud for other users devices', function () {
+test('user cannot toggle proxy cloud for other users devices', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
index 8b5843f..34782b1 100644
--- a/tests/Feature/ExampleTest.php
+++ b/tests/Feature/ExampleTest.php
@@ -1,6 +1,6 @@
get('/');
$response->assertStatus(200);
diff --git a/tests/Feature/FetchDeviceModelsCommandTest.php b/tests/Feature/FetchDeviceModelsCommandTest.php
index 2836330..e09ff4c 100644
--- a/tests/Feature/FetchDeviceModelsCommandTest.php
+++ b/tests/Feature/FetchDeviceModelsCommandTest.php
@@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
-test('command dispatches fetch device models job', function () {
+test('command dispatches fetch device models job', function (): void {
Queue::fake();
$this->artisan('device-models:fetch')
diff --git a/tests/Feature/FetchProxyCloudResponsesTest.php b/tests/Feature/FetchProxyCloudResponsesTest.php
index bd58002..561dc1c 100644
--- a/tests/Feature/FetchProxyCloudResponsesTest.php
+++ b/tests/Feature/FetchProxyCloudResponsesTest.php
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Storage;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-beforeEach(function () {
+beforeEach(function (): void {
Storage::fake('public');
Storage::disk('public')->makeDirectory('/images/generated');
Http::preventStrayRequests();
@@ -18,7 +18,7 @@ beforeEach(function () {
]);
});
-test('it fetches and processes proxy cloud responses for devices', function () {
+test('it fetches and processes proxy cloud responses for devices', function (): void {
config(['services.trmnl.proxy_base_url' => 'https://example.com']);
// Create a test device with proxy cloud enabled
@@ -59,16 +59,14 @@ test('it fetches and processes proxy cloud responses for devices', function () {
$job->handle();
// Assert HTTP requests were made with correct headers
- Http::assertSent(function ($request) use ($device) {
- return $request->hasHeader('id', $device->mac_address) &&
- $request->hasHeader('access-token', $device->api_key) &&
- $request->hasHeader('width', 800) &&
- $request->hasHeader('height', 480) &&
- $request->hasHeader('rssi', $device->last_rssi_level) &&
- $request->hasHeader('battery_voltage', $device->last_battery_voltage) &&
- $request->hasHeader('refresh-rate', $device->default_refresh_interval) &&
- $request->hasHeader('fw-version', $device->last_firmware_version);
- });
+ Http::assertSent(fn ($request): bool => $request->hasHeader('id', $device->mac_address) &&
+ $request->hasHeader('access-token', $device->api_key) &&
+ $request->hasHeader('width', 800) &&
+ $request->hasHeader('height', 480) &&
+ $request->hasHeader('rssi', $device->last_rssi_level) &&
+ $request->hasHeader('battery_voltage', $device->last_battery_voltage) &&
+ $request->hasHeader('refresh-rate', $device->default_refresh_interval) &&
+ $request->hasHeader('fw-version', $device->last_firmware_version));
// Assert the device was updated
$device->refresh();
@@ -82,7 +80,7 @@ test('it fetches and processes proxy cloud responses for devices', function () {
Storage::disk('public')->assertExists('images/generated/test-image.bmp');
});
-test('it handles log requests when present', function () {
+test('it handles log requests when present', function (): void {
$device = Device::factory()->create([
'proxy_cloud' => true,
'mac_address' => '00:11:22:33:44:55',
@@ -103,18 +101,16 @@ test('it handles log requests when present', function () {
$job->handle();
// Assert log request was sent
- Http::assertSent(function ($request) use ($device) {
- return $request->url() === config('services.trmnl.proxy_base_url').'/api/log' &&
- $request->hasHeader('id', $device->mac_address) &&
- $request->body() === json_encode(['message' => 'test log']);
- });
+ Http::assertSent(fn ($request): bool => $request->url() === config('services.trmnl.proxy_base_url').'/api/log' &&
+ $request->hasHeader('id', $device->mac_address) &&
+ $request->body() === json_encode(['message' => 'test log']));
// Assert log request was cleared
$device->refresh();
expect($device->last_log_request)->toBeNull();
});
-test('it handles API errors gracefully', function () {
+test('it handles API errors gracefully', function (): void {
$device = Device::factory()->create([
'proxy_cloud' => true,
'mac_address' => '00:11:22:33:44:55',
@@ -130,7 +126,7 @@ test('it handles API errors gracefully', function () {
expect(fn () => $job->handle())->not->toThrow(Exception::class);
});
-test('it only processes proxy cloud enabled devices', function () {
+test('it only processes proxy cloud enabled devices', function (): void {
Http::fake();
$enabledDevice = Device::factory()->create(['proxy_cloud' => true]);
$disabledDevice = Device::factory()->create(['proxy_cloud' => false]);
@@ -139,16 +135,12 @@ test('it only processes proxy cloud enabled devices', function () {
$job->handle();
// Assert request was only made for enabled device
- Http::assertSent(function ($request) use ($enabledDevice) {
- return $request->hasHeader('id', $enabledDevice->mac_address);
- });
+ Http::assertSent(fn ($request) => $request->hasHeader('id', $enabledDevice->mac_address));
- Http::assertNotSent(function ($request) use ($disabledDevice) {
- return $request->hasHeader('id', $disabledDevice->mac_address);
- });
+ Http::assertNotSent(fn ($request) => $request->hasHeader('id', $disabledDevice->mac_address));
});
-test('it fetches and processes proxy cloud responses for devices with BMP images', function () {
+test('it fetches and processes proxy cloud responses for devices with BMP images', function (): void {
config(['services.trmnl.proxy_base_url' => 'https://example.com']);
// Create a test device with proxy cloud enabled
@@ -176,16 +168,14 @@ test('it fetches and processes proxy cloud responses for devices with BMP images
$job->handle();
// Assert HTTP requests were made with correct headers
- Http::assertSent(function ($request) use ($device) {
- return $request->hasHeader('id', $device->mac_address) &&
- $request->hasHeader('access-token', $device->api_key) &&
- $request->hasHeader('width', 800) &&
- $request->hasHeader('height', 480) &&
- $request->hasHeader('rssi', $device->last_rssi_level) &&
- $request->hasHeader('battery_voltage', $device->last_battery_voltage) &&
- $request->hasHeader('refresh-rate', $device->default_refresh_interval) &&
- $request->hasHeader('fw-version', $device->last_firmware_version);
- });
+ Http::assertSent(fn ($request): bool => $request->hasHeader('id', $device->mac_address) &&
+ $request->hasHeader('access-token', $device->api_key) &&
+ $request->hasHeader('width', 800) &&
+ $request->hasHeader('height', 480) &&
+ $request->hasHeader('rssi', $device->last_rssi_level) &&
+ $request->hasHeader('battery_voltage', $device->last_battery_voltage) &&
+ $request->hasHeader('refresh-rate', $device->default_refresh_interval) &&
+ $request->hasHeader('fw-version', $device->last_firmware_version));
// Assert the device was updated
$device->refresh();
@@ -201,7 +191,7 @@ test('it fetches and processes proxy cloud responses for devices with BMP images
expect(Storage::disk('public')->exists('images/generated/test-image.png'))->toBeFalse();
});
-test('it fetches and processes proxy cloud responses for devices with PNG images', function () {
+test('it fetches and processes proxy cloud responses for devices with PNG images', function (): void {
config(['services.trmnl.proxy_base_url' => 'https://example.com']);
// Create a test device with proxy cloud enabled
@@ -229,16 +219,14 @@ test('it fetches and processes proxy cloud responses for devices with PNG images
$job->handle();
// Assert HTTP requests were made with correct headers
- Http::assertSent(function ($request) use ($device) {
- return $request->hasHeader('id', $device->mac_address) &&
- $request->hasHeader('access-token', $device->api_key) &&
- $request->hasHeader('width', 800) &&
- $request->hasHeader('height', 480) &&
- $request->hasHeader('rssi', $device->last_rssi_level) &&
- $request->hasHeader('battery_voltage', $device->last_battery_voltage) &&
- $request->hasHeader('refresh-rate', $device->default_refresh_interval) &&
- $request->hasHeader('fw-version', $device->last_firmware_version);
- });
+ Http::assertSent(fn ($request): bool => $request->hasHeader('id', $device->mac_address) &&
+ $request->hasHeader('access-token', $device->api_key) &&
+ $request->hasHeader('width', 800) &&
+ $request->hasHeader('height', 480) &&
+ $request->hasHeader('rssi', $device->last_rssi_level) &&
+ $request->hasHeader('battery_voltage', $device->last_battery_voltage) &&
+ $request->hasHeader('refresh-rate', $device->default_refresh_interval) &&
+ $request->hasHeader('fw-version', $device->last_firmware_version));
// Assert the device was updated
$device->refresh();
@@ -254,7 +242,7 @@ test('it fetches and processes proxy cloud responses for devices with PNG images
expect(Storage::disk('public')->exists('images/generated/test-image.bmp'))->toBeFalse();
});
-test('it handles missing content type in image URL gracefully', function () {
+test('it handles missing content type in image URL gracefully', function (): void {
config(['services.trmnl.proxy_base_url' => 'https://example.com']);
// Create a test device with proxy cloud enabled
diff --git a/tests/Feature/GenerateScreenJobTest.php b/tests/Feature/GenerateScreenJobTest.php
index 78ba932..115fb51 100644
--- a/tests/Feature/GenerateScreenJobTest.php
+++ b/tests/Feature/GenerateScreenJobTest.php
@@ -7,13 +7,13 @@ use Illuminate\Support\Facades\Storage;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-beforeEach(function () {
+beforeEach(function (): void {
TrmnlPipeline::fake();
Storage::fake('public');
Storage::disk('public')->makeDirectory('/images/generated');
});
-test('it generates screen images and updates device', function () {
+test('it generates screen images and updates device', function (): void {
$device = Device::factory()->create();
$job = new GenerateScreenJob($device->id, null, view('trmnl')->render());
$job->handle();
@@ -27,7 +27,7 @@ test('it generates screen images and updates device', function () {
Storage::disk('public')->assertExists("/images/generated/{$uuid}.png");
});
-test('it cleans up unused images', function () {
+test('it cleans up unused images', function (): void {
// Create some test devices with images
$activeDevice = Device::factory()->create([
'current_screen_image' => 'uuid-to-be-replaced',
@@ -49,7 +49,7 @@ test('it cleans up unused images', function () {
Storage::disk('public')->assertMissing('/images/generated/inactive-uuid.bmp');
});
-test('it preserves gitignore file during cleanup', function () {
+test('it preserves gitignore file during cleanup', function (): void {
Storage::disk('public')->put('/images/generated/.gitignore', '*');
$device = Device::factory()->create();
diff --git a/tests/Feature/Jobs/CleanupDeviceLogsJobTest.php b/tests/Feature/Jobs/CleanupDeviceLogsJobTest.php
index 15888c0..ae2833b 100644
--- a/tests/Feature/Jobs/CleanupDeviceLogsJobTest.php
+++ b/tests/Feature/Jobs/CleanupDeviceLogsJobTest.php
@@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('it keeps only the 50 most recent logs per device', function () {
+test('it keeps only the 50 most recent logs per device', function (): void {
// Create two devices
$device1 = Device::factory()->create();
$device2 = Device::factory()->create();
diff --git a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
index b85a24e..1c131c4 100644
--- a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
+++ b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
@@ -14,12 +14,12 @@ beforeEach(function (): void {
DeviceModel::truncate();
});
-test('fetch device models job can be dispatched', function () {
+test('fetch device models job can be dispatched', function (): void {
$job = new FetchDeviceModelsJob();
expect($job)->toBeInstanceOf(FetchDeviceModelsJob::class);
});
-test('fetch device models job handles successful api response', function () {
+test('fetch device models job handles successful api response', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
@@ -65,7 +65,7 @@ test('fetch device models job handles successful api response', function () {
expect($deviceModel->source)->toBe('api');
});
-test('fetch device models job handles multiple device models', function () {
+test('fetch device models job handles multiple device models', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
@@ -114,7 +114,7 @@ test('fetch device models job handles multiple device models', function () {
expect(DeviceModel::where('name', 'model-2')->exists())->toBeTrue();
});
-test('fetch device models job handles empty data array', function () {
+test('fetch device models job handles empty data array', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [],
@@ -131,7 +131,7 @@ test('fetch device models job handles empty data array', function () {
expect(DeviceModel::count())->toBe(0);
});
-test('fetch device models job handles missing data field', function () {
+test('fetch device models job handles missing data field', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'message' => 'No data available',
@@ -148,7 +148,7 @@ test('fetch device models job handles missing data field', function () {
expect(DeviceModel::count())->toBe(0);
});
-test('fetch device models job handles non-array data', function () {
+test('fetch device models job handles non-array data', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => 'invalid-data',
@@ -165,7 +165,7 @@ test('fetch device models job handles non-array data', function () {
expect(DeviceModel::count())->toBe(0);
});
-test('fetch device models job handles api failure', function () {
+test('fetch device models job handles api failure', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'error' => 'Internal Server Error',
@@ -185,9 +185,9 @@ test('fetch device models job handles api failure', function () {
expect(DeviceModel::count())->toBe(0);
});
-test('fetch device models job handles network exception', function () {
+test('fetch device models job handles network exception', function (): void {
Http::fake([
- 'usetrmnl.com/api/models' => function () {
+ 'usetrmnl.com/api/models' => function (): void {
throw new Exception('Network connection failed');
},
]);
@@ -202,7 +202,7 @@ test('fetch device models job handles network exception', function () {
expect(DeviceModel::count())->toBe(0);
});
-test('fetch device models job handles device model with missing name', function () {
+test('fetch device models job handles device model with missing name', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
@@ -228,7 +228,7 @@ test('fetch device models job handles device model with missing name', function
expect(DeviceModel::count())->toBe(0);
});
-test('fetch device models job handles device model with partial data', function () {
+test('fetch device models job handles device model with partial data', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
@@ -263,7 +263,7 @@ test('fetch device models job handles device model with partial data', function
expect($deviceModel->source)->toBe('api');
});
-test('fetch device models job updates existing device model', function () {
+test('fetch device models job updates existing device model', function (): void {
// Create an existing device model
$existingModel = DeviceModel::factory()->create([
'name' => 'existing-model',
@@ -309,7 +309,7 @@ test('fetch device models job updates existing device model', function () {
expect($existingModel->source)->toBe('api');
});
-test('fetch device models job handles processing exception for individual model', function () {
+test('fetch device models job handles processing exception for individual model', function (): void {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
diff --git a/tests/Feature/Jobs/FirmwareDownloadJobTest.php b/tests/Feature/Jobs/FirmwareDownloadJobTest.php
index 7ae9417..f9109bb 100644
--- a/tests/Feature/Jobs/FirmwareDownloadJobTest.php
+++ b/tests/Feature/Jobs/FirmwareDownloadJobTest.php
@@ -5,12 +5,12 @@ use App\Models\Firmware;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
-beforeEach(function () {
+beforeEach(function (): void {
Storage::fake('public');
Storage::disk('public')->makeDirectory('/firmwares');
});
-test('it creates firmwares directory if it does not exist', function () {
+test('it creates firmwares directory if it does not exist', function (): void {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
@@ -26,7 +26,7 @@ test('it creates firmwares directory if it does not exist', function () {
expect(Storage::disk('public')->exists('firmwares'))->toBeTrue();
});
-test('it downloads firmware and updates storage location', function () {
+test('it downloads firmware and updates storage location', function (): void {
Http::fake([
'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
]);
@@ -42,7 +42,7 @@ test('it downloads firmware and updates storage location', function () {
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
});
-test('it handles connection exception gracefully', function () {
+test('it handles connection exception gracefully', function (): void {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
@@ -50,7 +50,7 @@ test('it handles connection exception gracefully', function () {
]);
Http::fake([
- 'https://example.com/firmware.bin' => function () {
+ 'https://example.com/firmware.bin' => function (): void {
throw new Illuminate\Http\Client\ConnectionException('Connection failed');
},
]);
@@ -65,7 +65,7 @@ test('it handles connection exception gracefully', function () {
expect($firmware->fresh()->storage_location)->toBeNull();
});
-test('it handles general exception gracefully', function () {
+test('it handles general exception gracefully', function (): void {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
@@ -73,7 +73,7 @@ test('it handles general exception gracefully', function () {
]);
Http::fake([
- 'https://example.com/firmware.bin' => function () {
+ 'https://example.com/firmware.bin' => function (): void {
throw new Exception('Unexpected error');
},
]);
@@ -88,7 +88,7 @@ test('it handles general exception gracefully', function () {
expect($firmware->fresh()->storage_location)->toBeNull();
});
-test('it handles firmware with special characters in version tag', function () {
+test('it handles firmware with special characters in version tag', function (): void {
Http::fake([
'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
]);
@@ -103,7 +103,7 @@ test('it handles firmware with special characters in version tag', function () {
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0-beta.bin');
});
-test('it handles firmware with long version tag', function () {
+test('it handles firmware with long version tag', function (): void {
Http::fake([
'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
]);
@@ -118,7 +118,7 @@ test('it handles firmware with long version tag', function () {
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.1234.5678.90.bin');
});
-test('it creates firmwares directory even when it already exists', function () {
+test('it creates firmwares directory even when it already exists', function (): void {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
@@ -138,7 +138,7 @@ test('it creates firmwares directory even when it already exists', function () {
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
});
-test('it handles http error response', function () {
+test('it handles http error response', function (): void {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
diff --git a/tests/Feature/Jobs/FirmwarePollJobTest.php b/tests/Feature/Jobs/FirmwarePollJobTest.php
index 751bc8c..74c3cf7 100644
--- a/tests/Feature/Jobs/FirmwarePollJobTest.php
+++ b/tests/Feature/Jobs/FirmwarePollJobTest.php
@@ -5,11 +5,11 @@ use App\Models\Firmware;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
-beforeEach(function () {
+beforeEach(function (): void {
Http::preventStrayRequests();
});
-test('it creates new firmware record when polling', function () {
+test('it creates new firmware record when polling', function (): void {
Http::fake([
'https://usetrmnl.com/api/firmware/latest' => Http::response([
'version' => '1.0.0',
@@ -25,7 +25,7 @@ test('it creates new firmware record when polling', function () {
->latest->toBeTrue();
});
-test('it updates existing firmware record when polling', function () {
+test('it updates existing firmware record when polling', function (): void {
$existingFirmware = Firmware::factory()->create([
'version_tag' => '1.0.0',
'url' => 'https://old-url.com/firmware.bin',
@@ -46,7 +46,7 @@ test('it updates existing firmware record when polling', function () {
->latest->toBeTrue();
});
-test('it marks previous firmware as not latest when new version is found', function () {
+test('it marks previous firmware as not latest when new version is found', function (): void {
$oldFirmware = Firmware::factory()->create([
'version_tag' => '1.0.0',
'latest' => true,
@@ -65,9 +65,9 @@ test('it marks previous firmware as not latest when new version is found', funct
->and(Firmware::where('version_tag', '1.1.0')->first()->latest)->toBeTrue();
});
-test('it handles connection exception gracefully', function () {
+test('it handles connection exception gracefully', function (): void {
Http::fake([
- 'https://usetrmnl.com/api/firmware/latest' => function () {
+ 'https://usetrmnl.com/api/firmware/latest' => function (): void {
throw new ConnectionException('Connection failed');
},
]);
@@ -78,7 +78,7 @@ test('it handles connection exception gracefully', function () {
expect(Firmware::count())->toBe(0);
});
-test('it handles invalid response gracefully', function () {
+test('it handles invalid response gracefully', function (): void {
Http::fake([
'https://usetrmnl.com/api/firmware/latest' => Http::response(null, 200),
]);
@@ -89,7 +89,7 @@ test('it handles invalid response gracefully', function () {
expect(Firmware::count())->toBe(0);
});
-test('it handles missing version in response gracefully', function () {
+test('it handles missing version in response gracefully', function (): void {
Http::fake([
'https://usetrmnl.com/api/firmware/latest' => Http::response([
'url' => 'https://example.com/firmware.bin',
@@ -102,7 +102,7 @@ test('it handles missing version in response gracefully', function () {
expect(Firmware::count())->toBe(0);
});
-test('it handles missing url in response gracefully', function () {
+test('it handles missing url in response gracefully', function (): void {
Http::fake([
'https://usetrmnl.com/api/firmware/latest' => Http::response([
'version' => '1.0.0',
diff --git a/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php b/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
index 5ac9c17..6d69924 100644
--- a/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
+++ b/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
@@ -8,7 +8,7 @@ use App\Models\User;
use App\Notifications\BatteryLow;
use Illuminate\Support\Facades\Notification;
-test('it sends battery low notification when battery is below threshold', function () {
+test('it sends battery low notification when battery is below threshold', function (): void {
Notification::fake();
config(['app.notifications.battery_low.warn_at_percent' => 20]);
@@ -29,7 +29,7 @@ test('it sends battery low notification when battery is below threshold', functi
expect($device->battery_notification_sent)->toBeTrue();
});
-test('it does not send notification when battery is above threshold', function () {
+test('it does not send notification when battery is above threshold', function (): void {
Notification::fake();
config(['app.notifications.battery_low.warn_at_percent' => 20]);
@@ -50,7 +50,7 @@ test('it does not send notification when battery is above threshold', function (
expect($device->battery_notification_sent)->toBeFalse();
});
-test('it does not send notification when already sent', function () {
+test('it does not send notification when already sent', function (): void {
Notification::fake();
config(['app.notifications.battery_low.warn_at_percent' => 20]);
@@ -68,7 +68,7 @@ test('it does not send notification when already sent', function () {
Notification::assertNotSentTo($user, BatteryLow::class);
});
-test('it resets notification flag when battery is above threshold', function () {
+test('it resets notification flag when battery is above threshold', function (): void {
Notification::fake();
config(['app.notifications.battery_low.warn_at_percent' => 20]);
@@ -89,7 +89,7 @@ test('it resets notification flag when battery is above threshold', function ()
expect($device->battery_notification_sent)->toBeFalse();
});
-test('it skips devices without associated user', function () {
+test('it skips devices without associated user', function (): void {
Notification::fake();
config(['app.notifications.battery_low.warn_at_percent' => 20]);
@@ -106,7 +106,7 @@ test('it skips devices without associated user', function () {
Notification::assertNothingSent();
});
-test('it processes multiple devices correctly', function () {
+test('it processes multiple devices correctly', function (): void {
Notification::fake();
config(['app.notifications.battery_low.warn_at_percent' => 20]);
diff --git a/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php b/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
index d263334..5d8b057 100644
--- a/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
+++ b/tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
@@ -9,7 +9,7 @@ use Livewire\Livewire;
uses(RefreshDatabase::class);
-test('device auto join component can be rendered', function () {
+test('device auto join component can be rendered', function (): void {
$user = User::factory()->create(['assign_new_devices' => false]);
Livewire::actingAs($user)
@@ -19,7 +19,7 @@ test('device auto join component can be rendered', function () {
->assertSet('isFirstUser', true);
});
-test('device auto join component initializes with user settings', function () {
+test('device auto join component initializes with user settings', function (): void {
$user = User::factory()->create(['assign_new_devices' => true]);
Livewire::actingAs($user)
@@ -28,7 +28,7 @@ test('device auto join component initializes with user settings', function () {
->assertSet('isFirstUser', true);
});
-test('device auto join component identifies first user correctly', function () {
+test('device auto join component identifies first user correctly', function (): void {
$firstUser = User::factory()->create(['id' => 1, 'assign_new_devices' => false]);
$otherUser = User::factory()->create(['id' => 2, 'assign_new_devices' => false]);
@@ -41,7 +41,7 @@ test('device auto join component identifies first user correctly', function () {
->assertSet('isFirstUser', false);
});
-test('device auto join component updates user setting when toggled', function () {
+test('device auto join component updates user setting when toggled', function (): void {
$user = User::factory()->create(['assign_new_devices' => false]);
Livewire::actingAs($user)
@@ -55,7 +55,7 @@ test('device auto join component updates user setting when toggled', function ()
// Validation test removed - Livewire automatically handles boolean conversion
-test('device auto join component handles false value correctly', function () {
+test('device auto join component handles false value correctly', function (): void {
$user = User::factory()->create(['assign_new_devices' => true]);
Livewire::actingAs($user)
@@ -67,7 +67,7 @@ test('device auto join component handles false value correctly', function () {
expect($user->assign_new_devices)->toBeFalse();
});
-test('device auto join component only updates when deviceAutojoin property changes', function () {
+test('device auto join component only updates when deviceAutojoin property changes', function (): void {
$user = User::factory()->create(['assign_new_devices' => false]);
$component = Livewire::actingAs($user)
@@ -80,7 +80,7 @@ test('device auto join component only updates when deviceAutojoin property chang
expect($user->assign_new_devices)->toBeFalse();
});
-test('device auto join component renders correct view', function () {
+test('device auto join component renders correct view', function (): void {
$user = User::factory()->create();
Livewire::actingAs($user)
@@ -88,7 +88,7 @@ test('device auto join component renders correct view', function () {
->assertViewIs('livewire.actions.device-auto-join');
});
-test('device auto join component works with authenticated user', function () {
+test('device auto join component works with authenticated user', function (): void {
$user = User::factory()->create(['assign_new_devices' => true]);
$component = Livewire::actingAs($user)
@@ -98,7 +98,7 @@ test('device auto join component works with authenticated user', function () {
expect($component->instance()->isFirstUser)->toBe($user->id === 1);
});
-test('device auto join component handles multiple updates correctly', function () {
+test('device auto join component handles multiple updates correctly', function (): void {
$user = User::factory()->create(['assign_new_devices' => false]);
$component = Livewire::actingAs($user)
diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php
index 82bf816..8b26076 100644
--- a/tests/Feature/Livewire/Catalog/IndexTest.php
+++ b/tests/Feature/Livewire/Catalog/IndexTest.php
@@ -6,11 +6,11 @@ use Illuminate\Support\Facades\Http;
use Livewire\Volt\Volt;
use Symfony\Component\Yaml\Yaml;
-beforeEach(function () {
+beforeEach(function (): void {
Cache::flush();
});
-it('can render catalog component', function () {
+it('can render catalog component', function (): void {
// Mock empty catalog response
Http::fake([
config('app.catalog_url') => Http::response('', 200),
@@ -21,7 +21,7 @@ it('can render catalog component', function () {
$component->assertSee('No plugins available');
});
-it('loads plugins from catalog URL', function () {
+it('loads plugins from catalog URL', function (): void {
// Clear cache first to ensure fresh data
Cache::forget('catalog_plugins');
@@ -62,7 +62,7 @@ it('loads plugins from catalog URL', function () {
$component->assertSee('MIT');
});
-it('shows error when plugin not found', function () {
+it('shows error when plugin not found', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -75,7 +75,7 @@ it('shows error when plugin not found', function () {
$component->assertHasErrors();
});
-it('shows error when zip_url is missing', function () {
+it('shows error when zip_url is missing', function (): void {
$user = User::factory()->create();
// Mock the HTTP response for the catalog URL without zip_url
diff --git a/tests/Feature/PlaylistSchedulingTest.php b/tests/Feature/PlaylistSchedulingTest.php
index 43ad663..aea4923 100644
--- a/tests/Feature/PlaylistSchedulingTest.php
+++ b/tests/Feature/PlaylistSchedulingTest.php
@@ -10,7 +10,7 @@ use Illuminate\Support\Carbon;
uses(RefreshDatabase::class);
-test('playlist scheduling works correctly for time ranges spanning midnight', function () {
+test('playlist scheduling works correctly for time ranges spanning midnight', function (): void {
// Create a user and device
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
@@ -85,7 +85,7 @@ test('playlist scheduling works correctly for time ranges spanning midnight', fu
expect($nextItem->playlist->name)->toBe('Day until Deep Night Playlist');
});
-test('playlist isActiveNow handles midnight spanning correctly', function () {
+test('playlist isActiveNow handles midnight spanning correctly', function (): void {
$playlist = Playlist::factory()->create([
'is_active' => true,
'active_from' => '09:01',
@@ -110,7 +110,7 @@ test('playlist isActiveNow handles midnight spanning correctly', function () {
expect($playlist->isActiveNow())->toBeFalse();
});
-test('playlist isActiveNow handles normal time ranges correctly', function () {
+test('playlist isActiveNow handles normal time ranges correctly', function (): void {
$playlist = Playlist::factory()->create([
'is_active' => true,
'active_from' => '09:00',
diff --git a/tests/Feature/PluginArchiveTest.php b/tests/Feature/PluginArchiveTest.php
index 9a7f66c..9a95379 100644
--- a/tests/Feature/PluginArchiveTest.php
+++ b/tests/Feature/PluginArchiveTest.php
@@ -9,11 +9,11 @@ use App\Services\PluginImportService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
-beforeEach(function () {
+beforeEach(function (): void {
Storage::fake('local');
});
-it('exports plugin to zip file in correct format', function () {
+it('exports plugin to zip file in correct format', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -42,7 +42,7 @@ it('exports plugin to zip file in correct format', function () {
expect($response->getFile()->getFilename())->toContain('test-plugin-123.zip');
});
-it('exports plugin with polling configuration', function () {
+it('exports plugin with polling configuration', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -63,7 +63,7 @@ it('exports plugin with polling configuration', function () {
expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class);
});
-it('exports and imports plugin maintaining all data', function () {
+it('exports and imports plugin maintaining all data', function (): void {
$user = User::factory()->create();
$originalPlugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -122,7 +122,7 @@ it('exports and imports plugin maintaining all data', function () {
expect($importedPlugin->data_payload)->toBe(['items' => [1, 2, 3]]);
});
-it('handles blade templates correctly', function () {
+it('handles blade templates correctly', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -138,7 +138,7 @@ it('handles blade templates correctly', function () {
expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class);
});
-it('removes wrapper div from exported markup', function () {
+it('removes wrapper div from exported markup', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -154,7 +154,7 @@ it('removes wrapper div from exported markup', function () {
expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class);
});
-it('converts polling headers correctly', function () {
+it('converts polling headers correctly', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -170,7 +170,7 @@ it('converts polling headers correctly', function () {
expect($response)->toBeInstanceOf(Symfony\Component\HttpFoundation\BinaryFileResponse::class);
});
-it('api route returns zip file for authenticated user', function () {
+it('api route returns zip file for authenticated user', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -188,7 +188,7 @@ it('api route returns zip file for authenticated user', function () {
$response->assertHeader('Content-Disposition', 'attachment; filename=plugin_api-test-404.zip');
});
-it('api route returns 404 for non-existent plugin', function () {
+it('api route returns 404 for non-existent plugin', function (): void {
$user = User::factory()->create();
$response = $this->actingAs($user)
@@ -197,13 +197,13 @@ it('api route returns 404 for non-existent plugin', function () {
$response->assertStatus(404);
});
-it('api route returns 401 for unauthenticated user', function () {
+it('api route returns 401 for unauthenticated user', function (): void {
$response = $this->getJson('/api/plugin_settings/test-id/archive');
$response->assertStatus(401);
});
-it('api route returns 404 for plugin belonging to different user', function () {
+it('api route returns 404 for plugin belonging to different user', function (): void {
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$plugin = Plugin::factory()->create([
@@ -217,7 +217,7 @@ it('api route returns 404 for plugin belonging to different user', function () {
$response->assertStatus(404);
});
-it('exports zip with files in root directory', function () {
+it('exports zip with files in root directory', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
@@ -243,7 +243,7 @@ it('exports zip with files in root directory', function () {
$zip->close();
});
-it('maintains correct yaml field order', function () {
+it('maintains correct yaml field order', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
diff --git a/tests/Feature/PluginDefaultValuesTest.php b/tests/Feature/PluginDefaultValuesTest.php
index 30f62c0..353ad0c 100644
--- a/tests/Feature/PluginDefaultValuesTest.php
+++ b/tests/Feature/PluginDefaultValuesTest.php
@@ -6,7 +6,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('plugin import extracts default values from custom_fields and stores in configuration', function () {
+test('plugin import extracts default values from custom_fields and stores in configuration', function (): void {
// Create a user
$user = User::factory()->create();
@@ -74,7 +74,7 @@ test('plugin import extracts default values from custom_fields and stores in con
expect($plugin->configuration_template['custom_fields'])->toHaveCount(3);
});
-test('plugin import handles custom_fields without default values', function () {
+test('plugin import handles custom_fields without default values', function (): void {
// Create a user
$user = User::factory()->create();
diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php
index 86b9220..5c4a31f 100644
--- a/tests/Feature/PluginImportTest.php
+++ b/tests/Feature/PluginImportTest.php
@@ -9,11 +9,11 @@ use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
-beforeEach(function () {
+beforeEach(function (): void {
Storage::fake('local');
});
-it('imports plugin from valid zip file', function () {
+it('imports plugin from valid zip file', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file with the required structure
@@ -38,7 +38,7 @@ it('imports plugin from valid zip file', function () {
->and($plugin->configuration['api_key'])->toBe('default-api-key');
});
-it('imports plugin with shared.liquid file', function () {
+it('imports plugin with shared.liquid file', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
@@ -56,7 +56,7 @@ it('imports plugin with shared.liquid file', function () {
->and($plugin->render_markup)->toContain('');
});
-it('imports plugin with files in root directory', function () {
+it('imports plugin with files in root directory', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
@@ -73,17 +73,17 @@ it('imports plugin with files in root directory', function () {
->and($plugin->name)->toBe('Test Plugin');
});
-it('throws exception for invalid zip file', function () {
+it('throws exception for invalid zip file', function (): void {
$user = User::factory()->create();
$zipFile = UploadedFile::fake()->createWithContent('invalid.zip', 'not a zip file');
$pluginImportService = new PluginImportService();
- expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
+ expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
->toThrow(Exception::class, 'Could not open the ZIP file.');
});
-it('throws exception for missing required files', function () {
+it('throws exception for missing required files', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
@@ -94,11 +94,11 @@ it('throws exception for missing required files', function () {
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
$pluginImportService = new PluginImportService();
- expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
+ expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.');
});
-it('sets default values when settings are missing', function () {
+it('sets default values when settings are missing', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
@@ -117,7 +117,7 @@ it('sets default values when settings are missing', function () {
->and($plugin->polling_verb)->toBe('get'); // default value
});
-it('handles blade markup language correctly', function () {
+it('handles blade markup language correctly', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
@@ -133,7 +133,7 @@ it('handles blade markup language correctly', function () {
expect($plugin->markup_language)->toBe('blade');
});
-it('imports plugin from monorepo with zip_entry_path parameter', function () {
+it('imports plugin from monorepo with zip_entry_path parameter', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file with plugin in a subdirectory
@@ -152,7 +152,7 @@ it('imports plugin from monorepo with zip_entry_path parameter', function () {
->and($plugin->name)->toBe('Test Plugin');
});
-it('imports plugin from monorepo with src subdirectory', function () {
+it('imports plugin from monorepo with src subdirectory', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file with plugin in a subdirectory with src folder
@@ -171,7 +171,7 @@ it('imports plugin from monorepo with src subdirectory', function () {
->and($plugin->name)->toBe('Test Plugin');
});
-it('imports plugin from monorepo with shared.liquid in subdirectory', function () {
+it('imports plugin from monorepo with shared.liquid in subdirectory', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
@@ -189,7 +189,7 @@ it('imports plugin from monorepo with shared.liquid in subdirectory', function (
->and($plugin->render_markup)->toContain('
');
});
-it('imports plugin from URL with zip_entry_path parameter', function () {
+it('imports plugin from URL with zip_entry_path parameter', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file with plugin in a subdirectory
@@ -214,12 +214,10 @@ it('imports plugin from URL with zip_entry_path parameter', function () {
->and($plugin->user_id)->toBe($user->id)
->and($plugin->name)->toBe('Test Plugin');
- Http::assertSent(function ($request) {
- return $request->url() === 'https://github.com/example/repo/archive/refs/heads/main.zip';
- });
+ Http::assertSent(fn ($request): bool => $request->url() === 'https://github.com/example/repo/archive/refs/heads/main.zip');
});
-it('imports plugin from URL with zip_entry_path and src subdirectory', function () {
+it('imports plugin from URL with zip_entry_path and src subdirectory', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file with plugin in a subdirectory with src folder
@@ -245,7 +243,7 @@ it('imports plugin from URL with zip_entry_path and src subdirectory', function
->and($plugin->name)->toBe('Test Plugin');
});
-it('imports plugin from GitHub monorepo with repository-named directory', function () {
+it('imports plugin from GitHub monorepo with repository-named directory', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file that simulates GitHub's ZIP structure with repository-named directory
@@ -273,7 +271,7 @@ it('imports plugin from GitHub monorepo with repository-named directory', functi
->and($plugin->name)->toBe('Test Plugin'); // Should be from example-plugin, not other-plugin
});
-it('finds required files in simple ZIP structure', function () {
+it('finds required files in simple ZIP structure', function (): void {
$user = User::factory()->create();
// Create a simple ZIP file with just one plugin
@@ -292,7 +290,7 @@ it('finds required files in simple ZIP structure', function () {
->and($plugin->name)->toBe('Test Plugin');
});
-it('finds required files in GitHub monorepo structure with zip_entry_path', function () {
+it('finds required files in GitHub monorepo structure with zip_entry_path', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file that simulates GitHub's ZIP structure
@@ -313,7 +311,7 @@ it('finds required files in GitHub monorepo structure with zip_entry_path', func
->and($plugin->name)->toBe('Test Plugin'); // Should be from example-plugin, not other-plugin
});
-it('imports specific plugin from monorepo zip with zip_entry_path parameter', function () {
+it('imports specific plugin from monorepo zip with zip_entry_path parameter', function (): void {
$user = User::factory()->create();
// Create a mock ZIP file with 2 plugins in a monorepo structure
diff --git a/tests/Feature/PluginInlineTemplatesTest.php b/tests/Feature/PluginInlineTemplatesTest.php
index fb35344..76b29d7 100644
--- a/tests/Feature/PluginInlineTemplatesTest.php
+++ b/tests/Feature/PluginInlineTemplatesTest.php
@@ -5,7 +5,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('renders plugin with inline templates', function () {
+test('renders plugin with inline templates', function (): void {
$plugin = Plugin::factory()->create([
'name' => 'Test Plugin',
'markup_language' => 'liquid',
@@ -61,16 +61,16 @@ LIQUID
// Should render both templates
// Check for any of the facts (since random number generation is non-deterministic)
$this->assertTrue(
- str_contains($result, 'Fact 1') ||
- str_contains($result, 'Fact 2') ||
- str_contains($result, 'Fact 3')
+ str_contains((string) $result, 'Fact 1') ||
+ str_contains((string) $result, 'Fact 2') ||
+ str_contains((string) $result, 'Fact 3')
);
$this->assertStringContainsString('Test Plugin', $result);
$this->assertStringContainsString('Please try to enjoy each fact equally', $result);
$this->assertStringContainsString('class="view view--full"', $result);
});
-test('renders plugin with inline templates using with syntax', function () {
+test('renders plugin with inline templates using with syntax', function (): void {
$plugin = Plugin::factory()->create([
'name' => 'Test Plugin',
'markup_language' => 'liquid',
@@ -127,16 +127,16 @@ LIQUID
// Should render both templates
// Check for any of the facts (since random number generation is non-deterministic)
$this->assertTrue(
- str_contains($result, 'Fact 1') ||
- str_contains($result, 'Fact 2') ||
- str_contains($result, 'Fact 3')
+ str_contains((string) $result, 'Fact 1') ||
+ str_contains((string) $result, 'Fact 2') ||
+ str_contains((string) $result, 'Fact 3')
);
$this->assertStringContainsString('Test Plugin', $result);
$this->assertStringContainsString('Please try to enjoy each fact equally', $result);
$this->assertStringContainsString('class="view view--full"', $result);
});
-test('renders plugin with simple inline template', function () {
+test('renders plugin with simple inline template', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
@@ -162,7 +162,7 @@ LIQUID
$this->assertStringContainsString('class="simple"', $result);
});
-test('renders plugin with liquid filter find_by', function () {
+test('renders plugin with liquid filter find_by', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
@@ -194,7 +194,7 @@ LIQUID
$this->assertStringContainsString('class="user"', $result);
});
-test('renders plugin with liquid filter find_by and fallback', function () {
+test('renders plugin with liquid filter find_by and fallback', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
@@ -216,7 +216,7 @@ LIQUID
$this->assertStringContainsString('Not Found', $result);
});
-test('renders plugin with liquid filter group_by', function () {
+test('renders plugin with liquid filter group_by', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
diff --git a/tests/Feature/PluginLiquidFilterTest.php b/tests/Feature/PluginLiquidFilterTest.php
index fb429ae..bc0fc18 100644
--- a/tests/Feature/PluginLiquidFilterTest.php
+++ b/tests/Feature/PluginLiquidFilterTest.php
@@ -14,7 +14,7 @@ use Keepsuit\Liquid\Environment;
* to:
* {% assign _temp_xxx = collection | filter: "key", "value" %}{% for item in _temp_xxx %}
*/
-test('where filter works when assigned to variable first', function () {
+test('where filter works when assigned to variable first', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
@@ -42,7 +42,7 @@ LIQUID
$this->assertStringNotContainsString('"type":"L"', $result);
});
-test('where filter works directly in for loop with preprocessing', function () {
+test('where filter works directly in for loop with preprocessing', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
@@ -68,7 +68,7 @@ LIQUID
$this->assertStringNotContainsString('"type":"L"', $result);
});
-test('where filter works directly in for loop with multiple matches', function () {
+test('where filter works directly in for loop with multiple matches', function (): void {
$plugin = Plugin::factory()->create([
'markup_language' => 'liquid',
'render_markup' => <<<'LIQUID'
@@ -95,7 +95,7 @@ LIQUID
$this->assertStringNotContainsString('"type":"L"', $result);
});
-it('encodes arrays for url_encode as JSON with spaces after commas and then percent-encodes', function () {
+it('encodes arrays for url_encode as JSON with spaces after commas and then percent-encodes', function (): void {
/** @var Environment $env */
$env = app('liquid.environment');
$env->filterRegistry->register(StandardFilters::class);
@@ -109,7 +109,7 @@ it('encodes arrays for url_encode as JSON with spaces after commas and then perc
expect($output)->toBe('%5B%22common%22%2C%22obscure%22%5D');
});
-it('keeps scalar url_encode behavior intact', function () {
+it('keeps scalar url_encode behavior intact', function (): void {
/** @var Environment $env */
$env = app('liquid.environment');
$env->filterRegistry->register(StandardFilters::class);
diff --git a/tests/Feature/PluginRequiredConfigurationTest.php b/tests/Feature/PluginRequiredConfigurationTest.php
index 552b996..83be449 100644
--- a/tests/Feature/PluginRequiredConfigurationTest.php
+++ b/tests/Feature/PluginRequiredConfigurationTest.php
@@ -6,7 +6,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('hasMissingRequiredConfigurationFields returns true when required field is null', function () {
+test('hasMissingRequiredConfigurationFields returns true when required field is null', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -39,7 +39,7 @@ test('hasMissingRequiredConfigurationFields returns true when required field is
expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
});
-test('hasMissingRequiredConfigurationFields returns false when all required fields are set', function () {
+test('hasMissingRequiredConfigurationFields returns false when all required fields are set', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -73,7 +73,7 @@ test('hasMissingRequiredConfigurationFields returns false when all required fiel
expect($plugin->hasMissingRequiredConfigurationFields())->toBeFalse();
});
-test('hasMissingRequiredConfigurationFields returns false when no custom fields exist', function () {
+test('hasMissingRequiredConfigurationFields returns false when no custom fields exist', function (): void {
$user = User::factory()->create();
$plugin = Plugin::factory()->create([
@@ -85,7 +85,7 @@ test('hasMissingRequiredConfigurationFields returns false when no custom fields
expect($plugin->hasMissingRequiredConfigurationFields())->toBeFalse();
});
-test('hasMissingRequiredConfigurationFields returns true when explicitly required field is null', function () {
+test('hasMissingRequiredConfigurationFields returns true when explicitly required field is null', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -111,7 +111,7 @@ test('hasMissingRequiredConfigurationFields returns true when explicitly require
expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
});
-test('hasMissingRequiredConfigurationFields returns true when required field is empty string', function () {
+test('hasMissingRequiredConfigurationFields returns true when required field is empty string', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -137,7 +137,7 @@ test('hasMissingRequiredConfigurationFields returns true when required field is
expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
});
-test('hasMissingRequiredConfigurationFields returns true when required array field is empty', function () {
+test('hasMissingRequiredConfigurationFields returns true when required array field is empty', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -164,7 +164,7 @@ test('hasMissingRequiredConfigurationFields returns true when required array fie
expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
});
-test('hasMissingRequiredConfigurationFields returns false when author_bio field is present but other required field is set', function () {
+test('hasMissingRequiredConfigurationFields returns false when author_bio field is present but other required field is set', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -193,7 +193,7 @@ test('hasMissingRequiredConfigurationFields returns false when author_bio field
expect($plugin->hasMissingRequiredConfigurationFields())->toBeFalse();
});
-test('hasMissingRequiredConfigurationFields returns false when field has default value', function () {
+test('hasMissingRequiredConfigurationFields returns false when field has default value', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -217,7 +217,7 @@ test('hasMissingRequiredConfigurationFields returns false when field has default
expect($plugin->hasMissingRequiredConfigurationFields())->toBeFalse();
});
-test('hasMissingRequiredConfigurationFields returns true when required xhrSelect field is missing', function () {
+test('hasMissingRequiredConfigurationFields returns true when required xhrSelect field is missing', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
@@ -242,7 +242,7 @@ test('hasMissingRequiredConfigurationFields returns true when required xhrSelect
expect($plugin->hasMissingRequiredConfigurationFields())->toBeTrue();
});
-test('hasMissingRequiredConfigurationFields returns false when required xhrSelect field is set', function () {
+test('hasMissingRequiredConfigurationFields returns false when required xhrSelect field is set', function (): void {
$user = User::factory()->create();
$configurationTemplate = [
diff --git a/tests/Feature/PluginWebhookTest.php b/tests/Feature/PluginWebhookTest.php
index 70fa53a..22d1d54 100644
--- a/tests/Feature/PluginWebhookTest.php
+++ b/tests/Feature/PluginWebhookTest.php
@@ -3,7 +3,7 @@
use App\Models\Plugin;
use Illuminate\Support\Str;
-test('webhook updates plugin data for webhook strategy', function () {
+test('webhook updates plugin data for webhook strategy', function (): void {
// Create a plugin with webhook strategy
$plugin = Plugin::factory()->create([
'data_strategy' => 'webhook',
@@ -26,7 +26,7 @@ test('webhook updates plugin data for webhook strategy', function () {
]);
});
-test('webhook returns 400 for non-webhook strategy plugins', function () {
+test('webhook returns 400 for non-webhook strategy plugins', function (): void {
// Create a plugin with non-webhook strategy
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
@@ -43,7 +43,7 @@ test('webhook returns 400 for non-webhook strategy plugins', function () {
->assertJson(['error' => 'Plugin does not use webhook strategy']);
});
-test('webhook returns 400 when merge_variables is missing', function () {
+test('webhook returns 400 when merge_variables is missing', function (): void {
// Create a plugin with webhook strategy
$plugin = Plugin::factory()->create([
'data_strategy' => 'webhook',
@@ -58,7 +58,7 @@ test('webhook returns 400 when merge_variables is missing', function () {
->assertJson(['error' => 'Request must contain merge_variables key']);
});
-test('webhook returns 404 for non-existent plugin', function () {
+test('webhook returns 404 for non-existent plugin', function (): void {
// Make request with non-existent plugin UUID
$response = $this->postJson('/api/custom_plugins/'.Str::uuid(), [
'merge_variables' => ['new' => 'data'],
diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php
index 3252860..0e33955 100644
--- a/tests/Feature/Settings/PasswordUpdateTest.php
+++ b/tests/Feature/Settings/PasswordUpdateTest.php
@@ -6,7 +6,7 @@ use Livewire\Volt\Volt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('password can be updated', function () {
+test('password can be updated', function (): void {
$user = User::factory()->create([
'password' => Hash::make('password'),
]);
@@ -24,7 +24,7 @@ test('password can be updated', function () {
expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue();
});
-test('correct password must be provided to update password', function () {
+test('correct password must be provided to update password', function (): void {
$user = User::factory()->create([
'password' => Hash::make('password'),
]);
diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php
index 48ea114..cbf424c 100644
--- a/tests/Feature/Settings/ProfileUpdateTest.php
+++ b/tests/Feature/Settings/ProfileUpdateTest.php
@@ -5,13 +5,13 @@ use Livewire\Volt\Volt;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('profile page is displayed', function () {
+test('profile page is displayed', function (): void {
$this->actingAs($user = User::factory()->create());
$this->get('/settings/profile')->assertOk();
});
-test('profile information can be updated', function () {
+test('profile information can be updated', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -30,7 +30,7 @@ test('profile information can be updated', function () {
expect($user->email_verified_at)->toBeNull();
});
-test('email verification status is unchanged when email address is unchanged', function () {
+test('email verification status is unchanged when email address is unchanged', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -45,7 +45,7 @@ test('email verification status is unchanged when email address is unchanged', f
expect($user->refresh()->email_verified_at)->not->toBeNull();
});
-test('user can delete their account', function () {
+test('user can delete their account', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
@@ -62,7 +62,7 @@ test('user can delete their account', function () {
expect(auth()->check())->toBeFalse();
});
-test('correct password must be provided to delete account', function () {
+test('correct password must be provided to delete account', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php
index 44a4f33..963bc0c 100644
--- a/tests/Unit/ExampleTest.php
+++ b/tests/Unit/ExampleTest.php
@@ -1,5 +1,5 @@
toBeTrue();
});
diff --git a/tests/Unit/Liquid/Filters/DataTest.php b/tests/Unit/Liquid/Filters/DataTest.php
index bdf649f..abd4114 100644
--- a/tests/Unit/Liquid/Filters/DataTest.php
+++ b/tests/Unit/Liquid/Filters/DataTest.php
@@ -2,14 +2,14 @@
use App\Liquid\Filters\Data;
-test('json filter converts arrays to JSON', function () {
+test('json filter converts arrays to JSON', function (): void {
$filter = new Data();
$array = ['foo' => 'bar', 'baz' => 'qux'];
expect($filter->json($array))->toBe('{"foo":"bar","baz":"qux"}');
});
-test('json filter converts objects to JSON', function () {
+test('json filter converts objects to JSON', function (): void {
$filter = new Data();
$object = new stdClass();
$object->foo = 'bar';
@@ -18,7 +18,7 @@ test('json filter converts objects to JSON', function () {
expect($filter->json($object))->toBe('{"foo":"bar","baz":"qux"}');
});
-test('json filter handles nested structures', function () {
+test('json filter handles nested structures', function (): void {
$filter = new Data();
$nested = [
'foo' => 'bar',
@@ -31,7 +31,7 @@ test('json filter handles nested structures', function () {
expect($filter->json($nested))->toBe('{"foo":"bar","nested":{"baz":"qux","items":[1,2,3]}}');
});
-test('json filter handles scalar values', function () {
+test('json filter handles scalar values', function (): void {
$filter = new Data();
expect($filter->json('string'))->toBe('"string"');
@@ -40,21 +40,21 @@ test('json filter handles scalar values', function () {
expect($filter->json(null))->toBe('null');
});
-test('json filter preserves unicode characters', function () {
+test('json filter preserves unicode characters', function (): void {
$filter = new Data();
$data = ['message' => 'Hello, δΈη'];
expect($filter->json($data))->toBe('{"message":"Hello, δΈη"}');
});
-test('json filter does not escape slashes', function () {
+test('json filter does not escape slashes', function (): void {
$filter = new Data();
$data = ['url' => 'https://example.com/path'];
expect($filter->json($data))->toBe('{"url":"https://example.com/path"}');
});
-test('find_by filter finds object by key-value pair', function () {
+test('find_by filter finds object by key-value pair', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'age' => 35],
@@ -66,7 +66,7 @@ test('find_by filter finds object by key-value pair', function () {
expect($result)->toBe(['name' => 'Ryan', 'age' => 35]);
});
-test('find_by filter returns null when no match found', function () {
+test('find_by filter returns null when no match found', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'age' => 35],
@@ -78,7 +78,7 @@ test('find_by filter returns null when no match found', function () {
expect($result)->toBeNull();
});
-test('find_by filter returns fallback when no match found', function () {
+test('find_by filter returns fallback when no match found', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'age' => 35],
@@ -90,7 +90,7 @@ test('find_by filter returns fallback when no match found', function () {
expect($result)->toBe('Not Found');
});
-test('find_by filter finds by age', function () {
+test('find_by filter finds by age', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'age' => 35],
@@ -102,7 +102,7 @@ test('find_by filter finds by age', function () {
expect($result)->toBe(['name' => 'Sara', 'age' => 29]);
});
-test('find_by filter handles empty collection', function () {
+test('find_by filter handles empty collection', function (): void {
$filter = new Data();
$collection = [];
@@ -110,7 +110,7 @@ test('find_by filter handles empty collection', function () {
expect($result)->toBeNull();
});
-test('find_by filter handles collection with non-array items', function () {
+test('find_by filter handles collection with non-array items', function (): void {
$filter = new Data();
$collection = [
'not an array',
@@ -122,7 +122,7 @@ test('find_by filter handles collection with non-array items', function () {
expect($result)->toBe(['name' => 'Ryan', 'age' => 35]);
});
-test('find_by filter handles items without the specified key', function () {
+test('find_by filter handles items without the specified key', function (): void {
$filter = new Data();
$collection = [
['age' => 35],
@@ -134,7 +134,7 @@ test('find_by filter handles items without the specified key', function () {
expect($result)->toBe(['name' => 'Ryan', 'age' => 35]);
});
-test('group_by filter groups collection by age', function () {
+test('group_by filter groups collection by age', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'age' => 35],
@@ -153,7 +153,7 @@ test('group_by filter groups collection by age', function () {
]);
});
-test('group_by filter groups collection by name', function () {
+test('group_by filter groups collection by name', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'age' => 35],
@@ -172,7 +172,7 @@ test('group_by filter groups collection by name', function () {
]);
});
-test('group_by filter handles empty collection', function () {
+test('group_by filter handles empty collection', function (): void {
$filter = new Data();
$collection = [];
@@ -180,7 +180,7 @@ test('group_by filter handles empty collection', function () {
expect($result)->toBe([]);
});
-test('group_by filter handles collection with non-array items', function () {
+test('group_by filter handles collection with non-array items', function (): void {
$filter = new Data();
$collection = [
'not an array',
@@ -197,7 +197,7 @@ test('group_by filter handles collection with non-array items', function () {
]);
});
-test('group_by filter handles items without the specified key', function () {
+test('group_by filter handles items without the specified key', function (): void {
$filter = new Data();
$collection = [
['age' => 35],
@@ -217,7 +217,7 @@ test('group_by filter handles items without the specified key', function () {
]);
});
-test('group_by filter handles mixed data types as keys', function () {
+test('group_by filter handles mixed data types as keys', function (): void {
$filter = new Data();
$collection = [
['name' => 'Ryan', 'active' => true],
@@ -238,7 +238,7 @@ test('group_by filter handles mixed data types as keys', function () {
]);
});
-test('sample filter returns a random element from array', function () {
+test('sample filter returns a random element from array', function (): void {
$filter = new Data();
$array = ['1', '2', '3', '4', '5'];
@@ -246,7 +246,7 @@ test('sample filter returns a random element from array', function () {
expect($result)->toBeIn($array);
});
-test('sample filter returns a random element from string array', function () {
+test('sample filter returns a random element from string array', function (): void {
$filter = new Data();
$array = ['cat', 'dog'];
@@ -254,7 +254,7 @@ test('sample filter returns a random element from string array', function () {
expect($result)->toBeIn($array);
});
-test('sample filter returns null for empty array', function () {
+test('sample filter returns null for empty array', function (): void {
$filter = new Data();
$array = [];
@@ -262,7 +262,7 @@ test('sample filter returns null for empty array', function () {
expect($result)->toBeNull();
});
-test('sample filter returns the only element from single element array', function () {
+test('sample filter returns the only element from single element array', function (): void {
$filter = new Data();
$array = ['single'];
@@ -270,7 +270,7 @@ test('sample filter returns the only element from single element array', functio
expect($result)->toBe('single');
});
-test('sample filter works with mixed data types', function () {
+test('sample filter works with mixed data types', function (): void {
$filter = new Data();
$array = [1, 'string', true, null, ['nested']];
@@ -278,7 +278,7 @@ test('sample filter works with mixed data types', function () {
expect($result)->toBeIn($array);
});
-test('parse_json filter parses JSON string to array', function () {
+test('parse_json filter parses JSON string to array', function (): void {
$filter = new Data();
$jsonString = '[{"a":1,"b":"c"},"d"]';
@@ -286,7 +286,7 @@ test('parse_json filter parses JSON string to array', function () {
expect($result)->toBe([['a' => 1, 'b' => 'c'], 'd']);
});
-test('parse_json filter parses simple JSON object', function () {
+test('parse_json filter parses simple JSON object', function (): void {
$filter = new Data();
$jsonString = '{"name":"John","age":30,"city":"New York"}';
@@ -294,7 +294,7 @@ test('parse_json filter parses simple JSON object', function () {
expect($result)->toBe(['name' => 'John', 'age' => 30, 'city' => 'New York']);
});
-test('parse_json filter parses JSON array', function () {
+test('parse_json filter parses JSON array', function (): void {
$filter = new Data();
$jsonString = '["apple","banana","cherry"]';
@@ -302,7 +302,7 @@ test('parse_json filter parses JSON array', function () {
expect($result)->toBe(['apple', 'banana', 'cherry']);
});
-test('parse_json filter parses nested JSON structure', function () {
+test('parse_json filter parses nested JSON structure', function (): void {
$filter = new Data();
$jsonString = '{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"total":2}';
@@ -316,7 +316,7 @@ test('parse_json filter parses nested JSON structure', function () {
]);
});
-test('parse_json filter handles primitive values', function () {
+test('parse_json filter handles primitive values', function (): void {
$filter = new Data();
expect($filter->parse_json('"hello"'))->toBe('hello');
diff --git a/tests/Unit/Liquid/Filters/DateTest.php b/tests/Unit/Liquid/Filters/DateTest.php
index 5813e10..d967951 100644
--- a/tests/Unit/Liquid/Filters/DateTest.php
+++ b/tests/Unit/Liquid/Filters/DateTest.php
@@ -3,28 +3,28 @@
use App\Liquid\Filters\Date;
use Carbon\Carbon;
-test('days_ago filter returns correct date', function () {
+test('days_ago filter returns correct date', function (): void {
$filter = new Date();
$threeDaysAgo = Carbon::now()->subDays(3)->toDateString();
expect($filter->days_ago(3))->toBe($threeDaysAgo);
});
-test('days_ago filter handles string input', function () {
+test('days_ago filter handles string input', function (): void {
$filter = new Date();
$fiveDaysAgo = Carbon::now()->subDays(5)->toDateString();
expect($filter->days_ago('5'))->toBe($fiveDaysAgo);
});
-test('days_ago filter with zero days returns today', function () {
+test('days_ago filter with zero days returns today', function (): void {
$filter = new Date();
$today = Carbon::now()->toDateString();
expect($filter->days_ago(0))->toBe($today);
});
-test('days_ago filter with large number works correctly', function () {
+test('days_ago filter with large number works correctly', function (): void {
$filter = new Date();
$hundredDaysAgo = Carbon::now()->subDays(100)->toDateString();
diff --git a/tests/Unit/Liquid/Filters/LocalizationTest.php b/tests/Unit/Liquid/Filters/LocalizationTest.php
index 2ba3dd2..a52623f 100644
--- a/tests/Unit/Liquid/Filters/LocalizationTest.php
+++ b/tests/Unit/Liquid/Filters/LocalizationTest.php
@@ -2,7 +2,7 @@
use App\Liquid\Filters\Localization;
-test('l_date formats date with default format', function () {
+test('l_date formats date with default format', function (): void {
$filter = new Localization();
$date = '2025-01-11';
@@ -15,7 +15,7 @@ test('l_date formats date with default format', function () {
expect($result)->toContain('11');
});
-test('l_date formats date with custom format', function () {
+test('l_date formats date with custom format', function (): void {
$filter = new Localization();
$date = '2025-01-11';
@@ -27,7 +27,7 @@ test('l_date formats date with custom format', function () {
// We can't check for 'Jan' specifically as it might be localized
});
-test('l_date handles DateTime objects', function () {
+test('l_date handles DateTime objects', function (): void {
$filter = new Localization();
$date = new DateTimeImmutable('2025-01-11');
@@ -36,32 +36,32 @@ test('l_date handles DateTime objects', function () {
expect($result)->toContain('2025-01-11');
});
-test('l_word translates common words', function () {
+test('l_word translates common words', function (): void {
$filter = new Localization();
expect($filter->l_word('today', 'de'))->toBe('heute');
});
-test('l_word returns original word if no translation exists', function () {
+test('l_word returns original word if no translation exists', function (): void {
$filter = new Localization();
expect($filter->l_word('hello', 'es-ES'))->toBe('hello');
expect($filter->l_word('world', 'ko'))->toBe('world');
});
-test('l_word is case-insensitive', function () {
+test('l_word is case-insensitive', function (): void {
$filter = new Localization();
expect($filter->l_word('TODAY', 'de'))->toBe('heute');
});
-test('l_word returns original word for unknown locales', function () {
+test('l_word returns original word for unknown locales', function (): void {
$filter = new Localization();
expect($filter->l_word('today', 'unknown-locale'))->toBe('today');
});
-test('l_date handles locale parameter', function () {
+test('l_date handles locale parameter', function (): void {
$filter = new Localization();
$date = '2025-01-11';
@@ -73,7 +73,7 @@ test('l_date handles locale parameter', function () {
expect($result)->toContain('11');
});
-test('l_date handles null locale parameter', function () {
+test('l_date handles null locale parameter', function (): void {
$filter = new Localization();
$date = '2025-01-11';
@@ -85,7 +85,7 @@ test('l_date handles null locale parameter', function () {
expect($result)->toContain('11');
});
-test('l_date handles different date formats with locale', function () {
+test('l_date handles different date formats with locale', function (): void {
$filter = new Localization();
$date = '2025-01-11';
@@ -96,7 +96,7 @@ test('l_date handles different date formats with locale', function () {
expect($result)->toContain('11');
});
-test('l_date handles DateTimeInterface objects with locale', function () {
+test('l_date handles DateTimeInterface objects with locale', function (): void {
$filter = new Localization();
$date = new DateTimeImmutable('2025-01-11');
@@ -108,29 +108,29 @@ test('l_date handles DateTimeInterface objects with locale', function () {
expect($result)->toContain('11');
});
-test('l_date handles invalid date gracefully', function () {
+test('l_date handles invalid date gracefully', function (): void {
$filter = new Localization();
$invalidDate = 'invalid-date';
// This should throw an exception or return a default value
// The exact behavior depends on Carbon's implementation
- expect(fn () => $filter->l_date($invalidDate))->toThrow(Exception::class);
+ expect(fn (): string => $filter->l_date($invalidDate))->toThrow(Exception::class);
});
-test('l_word handles empty string', function () {
+test('l_word handles empty string', function (): void {
$filter = new Localization();
expect($filter->l_word('', 'de'))->toBe('');
});
-test('l_word handles special characters', function () {
+test('l_word handles special characters', function (): void {
$filter = new Localization();
// Test with a word that has special characters
expect($filter->l_word('cafΓ©', 'de'))->toBe('cafΓ©');
});
-test('l_word handles numeric strings', function () {
+test('l_word handles numeric strings', function (): void {
$filter = new Localization();
expect($filter->l_word('123', 'de'))->toBe('123');
diff --git a/tests/Unit/Liquid/Filters/NumbersTest.php b/tests/Unit/Liquid/Filters/NumbersTest.php
index 7ce736a..42deffb 100644
--- a/tests/Unit/Liquid/Filters/NumbersTest.php
+++ b/tests/Unit/Liquid/Filters/NumbersTest.php
@@ -2,7 +2,7 @@
use App\Liquid\Filters\Numbers;
-test('number_with_delimiter formats numbers with commas by default', function () {
+test('number_with_delimiter formats numbers with commas by default', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(1234))->toBe('1,234');
@@ -10,21 +10,21 @@ test('number_with_delimiter formats numbers with commas by default', function ()
expect($filter->number_with_delimiter(0))->toBe('0');
});
-test('number_with_delimiter handles custom delimiters', function () {
+test('number_with_delimiter handles custom delimiters', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(1234, '.'))->toBe('1.234');
expect($filter->number_with_delimiter(1000000, ' '))->toBe('1 000 000');
});
-test('number_with_delimiter handles decimal values with custom separators', function () {
+test('number_with_delimiter handles decimal values with custom separators', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(1234.57, ' ', ','))->toBe('1 234,57');
expect($filter->number_with_delimiter(1234.5, '.', ','))->toBe('1.234,50');
});
-test('number_to_currency formats numbers with dollar sign by default', function () {
+test('number_to_currency formats numbers with dollar sign by default', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency(1234))->toBe('$1,234');
@@ -32,14 +32,14 @@ test('number_to_currency formats numbers with dollar sign by default', function
expect($filter->number_to_currency(0))->toBe('$0');
});
-test('number_to_currency handles custom currency symbols', function () {
+test('number_to_currency handles custom currency symbols', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency(1234, 'Β£'))->toBe('Β£1,234');
expect($filter->number_to_currency(152350.69, 'β¬'))->toBe('β¬152,350.69');
});
-test('number_to_currency handles custom delimiters and separators', function () {
+test('number_to_currency handles custom delimiters and separators', function (): void {
$filter = new Numbers();
$result1 = $filter->number_to_currency(1234.57, 'Β£', '.', ',');
@@ -51,56 +51,56 @@ test('number_to_currency handles custom delimiters and separators', function ()
expect($result2)->toContain('β¬');
});
-test('number_with_delimiter handles string numbers', function () {
+test('number_with_delimiter handles string numbers', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter('1234'))->toBe('1,234');
expect($filter->number_with_delimiter('1234.56'))->toBe('1,234.56');
});
-test('number_with_delimiter handles negative numbers', function () {
+test('number_with_delimiter handles negative numbers', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(-1234))->toBe('-1,234');
expect($filter->number_with_delimiter(-1234.56))->toBe('-1,234.56');
});
-test('number_with_delimiter handles zero', function () {
+test('number_with_delimiter handles zero', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(0))->toBe('0');
expect($filter->number_with_delimiter(0.0))->toBe('0.00');
});
-test('number_with_delimiter handles very small numbers', function () {
+test('number_with_delimiter handles very small numbers', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(0.01))->toBe('0.01');
expect($filter->number_with_delimiter(0.001))->toBe('0.00');
});
-test('number_to_currency handles string numbers', function () {
+test('number_to_currency handles string numbers', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency('1234'))->toBe('$1,234');
expect($filter->number_to_currency('1234.56'))->toBe('$1,234.56');
});
-test('number_to_currency handles negative numbers', function () {
+test('number_to_currency handles negative numbers', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency(-1234))->toBe('-$1,234');
expect($filter->number_to_currency(-1234.56))->toBe('-$1,234.56');
});
-test('number_to_currency handles zero', function () {
+test('number_to_currency handles zero', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency(0))->toBe('$0');
expect($filter->number_to_currency(0.0))->toBe('$0.00');
});
-test('number_to_currency handles currency code conversion', function () {
+test('number_to_currency handles currency code conversion', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency(1234, '$'))->toBe('$1,234');
@@ -108,7 +108,7 @@ test('number_to_currency handles currency code conversion', function () {
expect($filter->number_to_currency(1234, 'Β£'))->toBe('Β£1,234');
});
-test('number_to_currency handles German locale formatting', function () {
+test('number_to_currency handles German locale formatting', function (): void {
$filter = new Numbers();
// When delimiter is '.' and separator is ',', it should use German locale
@@ -116,21 +116,21 @@ test('number_to_currency handles German locale formatting', function () {
expect($result)->toContain('1.234,56');
});
-test('number_with_delimiter handles different decimal separators', function () {
+test('number_with_delimiter handles different decimal separators', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(1234.56, ',', ','))->toBe('1,234,56');
expect($filter->number_with_delimiter(1234.56, ' ', ','))->toBe('1 234,56');
});
-test('number_to_currency handles very large numbers', function () {
+test('number_to_currency handles very large numbers', function (): void {
$filter = new Numbers();
expect($filter->number_to_currency(1000000))->toBe('$1,000,000');
expect($filter->number_to_currency(1000000.50))->toBe('$1,000,000.50');
});
-test('number_with_delimiter handles very large numbers', function () {
+test('number_with_delimiter handles very large numbers', function (): void {
$filter = new Numbers();
expect($filter->number_with_delimiter(1000000))->toBe('1,000,000');
diff --git a/tests/Unit/Liquid/Filters/StringMarkupTest.php b/tests/Unit/Liquid/Filters/StringMarkupTest.php
index b3498c3..bfd1a07 100644
--- a/tests/Unit/Liquid/Filters/StringMarkupTest.php
+++ b/tests/Unit/Liquid/Filters/StringMarkupTest.php
@@ -2,35 +2,35 @@
use App\Liquid\Filters\StringMarkup;
-test('pluralize returns singular form with count 1', function () {
+test('pluralize returns singular form with count 1', function (): void {
$filter = new StringMarkup();
expect($filter->pluralize('book', 1))->toBe('1 book');
expect($filter->pluralize('person', 1))->toBe('1 person');
});
-test('pluralize returns plural form with count greater than 1', function () {
+test('pluralize returns plural form with count greater than 1', function (): void {
$filter = new StringMarkup();
expect($filter->pluralize('book', 2))->toBe('2 books');
expect($filter->pluralize('person', 4))->toBe('4 people');
});
-test('pluralize handles irregular plurals correctly', function () {
+test('pluralize handles irregular plurals correctly', function (): void {
$filter = new StringMarkup();
expect($filter->pluralize('child', 3))->toBe('3 children');
expect($filter->pluralize('sheep', 5))->toBe('5 sheep');
});
-test('pluralize uses default count of 2 when not specified', function () {
+test('pluralize uses default count of 2 when not specified', function (): void {
$filter = new StringMarkup();
expect($filter->pluralize('book'))->toBe('2 books');
expect($filter->pluralize('person'))->toBe('2 people');
});
-test('markdown_to_html converts basic markdown to HTML', function () {
+test('markdown_to_html converts basic markdown to HTML', function (): void {
$filter = new StringMarkup();
$markdown = 'This is *italic* and **bold**.';
@@ -42,7 +42,7 @@ test('markdown_to_html converts basic markdown to HTML', function () {
expect($result)->toContain('
bold');
});
-test('markdown_to_html converts links correctly', function () {
+test('markdown_to_html converts links correctly', function (): void {
$filter = new StringMarkup();
$markdown = 'This is [a link](https://example.com).';
@@ -51,7 +51,7 @@ test('markdown_to_html converts links correctly', function () {
expect($result)->toContain('
a link');
});
-test('markdown_to_html handles fallback when Parsedown is not available', function () {
+test('markdown_to_html handles fallback when Parsedown is not available', function (): void {
// Create a mock that simulates Parsedown not being available
$filter = new class extends StringMarkup
{
@@ -68,28 +68,28 @@ test('markdown_to_html handles fallback when Parsedown is not available', functi
expect($result)->toBe('This is *italic* and [a link](https://example.com).');
});
-test('strip_html removes HTML tags', function () {
+test('strip_html removes HTML tags', function (): void {
$filter = new StringMarkup();
$html = '
This is bold and italic.
';
expect($filter->strip_html($html))->toBe('This is bold and italic.');
});
-test('strip_html preserves text content', function () {
+test('strip_html preserves text content', function (): void {
$filter = new StringMarkup();
$html = '
Hello, world!
';
expect($filter->strip_html($html))->toBe('Hello, world!');
});
-test('strip_html handles nested tags', function () {
+test('strip_html handles nested tags', function (): void {
$filter = new StringMarkup();
$html = '
Paragraph with nested tags.
';
expect($filter->strip_html($html))->toBe('Paragraph with nested tags.');
});
-test('markdown_to_html handles CommonMarkException gracefully', function () {
+test('markdown_to_html handles CommonMarkException gracefully', function (): void {
$filter = new StringMarkup();
// Create a mock that throws CommonMarkException
@@ -113,7 +113,7 @@ test('markdown_to_html handles CommonMarkException gracefully', function () {
expect($result)->toBeNull();
});
-test('markdown_to_html handles empty string', function () {
+test('markdown_to_html handles empty string', function (): void {
$filter = new StringMarkup();
$result = $filter->markdown_to_html('');
@@ -121,7 +121,7 @@ test('markdown_to_html handles empty string', function () {
expect($result)->toBe('');
});
-test('markdown_to_html handles complex markdown', function () {
+test('markdown_to_html handles complex markdown', function (): void {
$filter = new StringMarkup();
$markdown = "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link](https://example.com)";
@@ -135,34 +135,34 @@ test('markdown_to_html handles complex markdown', function () {
expect($result)->toContain('
Link');
});
-test('strip_html handles empty string', function () {
+test('strip_html handles empty string', function (): void {
$filter = new StringMarkup();
expect($filter->strip_html(''))->toBe('');
});
-test('strip_html handles string without HTML tags', function () {
+test('strip_html handles string without HTML tags', function (): void {
$filter = new StringMarkup();
$text = 'This is plain text without any HTML tags.';
expect($filter->strip_html($text))->toBe($text);
});
-test('strip_html handles self-closing tags', function () {
+test('strip_html handles self-closing tags', function (): void {
$filter = new StringMarkup();
$html = '
Text with
line break and
horizontal rule.';
expect($filter->strip_html($html))->toBe('Text with line break and horizontal rule.');
});
-test('pluralize handles zero count', function () {
+test('pluralize handles zero count', function (): void {
$filter = new StringMarkup();
expect($filter->pluralize('book', 0))->toBe('0 books');
expect($filter->pluralize('person', 0))->toBe('0 people');
});
-test('pluralize handles negative count', function () {
+test('pluralize handles negative count', function (): void {
$filter = new StringMarkup();
expect($filter->pluralize('book', -1))->toBe('-1 book');
diff --git a/tests/Unit/Liquid/Filters/UniquenessTest.php b/tests/Unit/Liquid/Filters/UniquenessTest.php
index 291f312..76840e1 100644
--- a/tests/Unit/Liquid/Filters/UniquenessTest.php
+++ b/tests/Unit/Liquid/Filters/UniquenessTest.php
@@ -2,7 +2,7 @@
use App\Liquid\Filters\Uniqueness;
-test('append_random appends a random string with 4 characters', function () {
+test('append_random appends a random string with 4 characters', function (): void {
$filter = new Uniqueness();
$result = $filter->append_random('chart-');
diff --git a/tests/Unit/Models/DeviceLogTest.php b/tests/Unit/Models/DeviceLogTest.php
index 37e128f..f28f4cd 100644
--- a/tests/Unit/Models/DeviceLogTest.php
+++ b/tests/Unit/Models/DeviceLogTest.php
@@ -6,7 +6,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-test('device log belongs to a device', function () {
+test('device log belongs to a device', function (): void {
$device = Device::factory()->create();
$log = DeviceLog::factory()->create(['device_id' => $device->id]);
@@ -14,7 +14,7 @@ test('device log belongs to a device', function () {
->and($log->device->id)->toBe($device->id);
});
-test('device log casts log_entry to array', function () {
+test('device log casts log_entry to array', function (): void {
Device::factory()->create();
$log = DeviceLog::factory()->create([
'log_entry' => [
@@ -29,7 +29,7 @@ test('device log casts log_entry to array', function () {
->and($log->log_entry['level'])->toBe('info');
});
-test('device log casts device_timestamp to datetime', function () {
+test('device log casts device_timestamp to datetime', function (): void {
Device::factory()->create();
$timestamp = now();
$log = DeviceLog::factory()->create([
@@ -40,7 +40,7 @@ test('device log casts device_timestamp to datetime', function () {
->and($log->device_timestamp->timestamp)->toBe($timestamp->timestamp);
});
-test('device log factory creates valid data', function () {
+test('device log factory creates valid data', function (): void {
Device::factory()->create();
$log = DeviceLog::factory()->create();
@@ -50,7 +50,7 @@ test('device log factory creates valid data', function () {
->and($log->log_entry)->toHaveKeys(['creation_timestamp', 'device_status_stamp', 'log_id', 'log_message', 'log_codeline', 'log_sourcefile', 'additional_info']);
});
-test('device log can be created with minimal required fields', function () {
+test('device log can be created with minimal required fields', function (): void {
$device = Device::factory()->create();
$log = DeviceLog::create([
'device_id' => $device->id,
diff --git a/tests/Unit/Models/DeviceModelTest.php b/tests/Unit/Models/DeviceModelTest.php
index 24904d6..8c2b6e9 100644
--- a/tests/Unit/Models/DeviceModelTest.php
+++ b/tests/Unit/Models/DeviceModelTest.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
use App\Models\DeviceModel;
-test('device model has required attributes', function () {
+test('device model has required attributes', function (): void {
$deviceModel = DeviceModel::factory()->create([
'name' => 'Test Model',
'width' => 800,
@@ -28,7 +28,7 @@ test('device model has required attributes', function () {
expect($deviceModel->offset_y)->toBe(0);
});
-test('device model casts attributes correctly', function () {
+test('device model casts attributes correctly', function (): void {
$deviceModel = DeviceModel::factory()->create([
'width' => '800',
'height' => '480',
@@ -50,61 +50,61 @@ test('device model casts attributes correctly', function () {
expect($deviceModel->offset_y)->toBeInt();
});
-test('get color depth attribute returns correct format for bit depth 2', function () {
+test('get color depth attribute returns correct format for bit depth 2', function (): void {
$deviceModel = DeviceModel::factory()->create(['bit_depth' => 2]);
expect($deviceModel->getColorDepthAttribute())->toBe('2bit');
});
-test('get color depth attribute returns correct format for bit depth 4', function () {
+test('get color depth attribute returns correct format for bit depth 4', function (): void {
$deviceModel = DeviceModel::factory()->create(['bit_depth' => 4]);
expect($deviceModel->getColorDepthAttribute())->toBe('4bit');
});
-test('get color depth attribute returns 4bit for bit depth greater than 4', function () {
+test('get color depth attribute returns 4bit for bit depth greater than 4', function (): void {
$deviceModel = DeviceModel::factory()->create(['bit_depth' => 8]);
expect($deviceModel->getColorDepthAttribute())->toBe('4bit');
});
-test('get color depth attribute returns null when bit depth is null', function () {
+test('get color depth attribute returns null when bit depth is null', function (): void {
$deviceModel = new DeviceModel(['bit_depth' => null]);
expect($deviceModel->getColorDepthAttribute())->toBeNull();
});
-test('get scale level attribute returns null for width 800 or less', function () {
+test('get scale level attribute returns null for width 800 or less', function (): void {
$deviceModel = DeviceModel::factory()->create(['width' => 800]);
expect($deviceModel->getScaleLevelAttribute())->toBeNull();
});
-test('get scale level attribute returns large for width between 801 and 1000', function () {
+test('get scale level attribute returns large for width between 801 and 1000', function (): void {
$deviceModel = DeviceModel::factory()->create(['width' => 900]);
expect($deviceModel->getScaleLevelAttribute())->toBe('large');
});
-test('get scale level attribute returns xlarge for width between 1001 and 1400', function () {
+test('get scale level attribute returns xlarge for width between 1001 and 1400', function (): void {
$deviceModel = DeviceModel::factory()->create(['width' => 1200]);
expect($deviceModel->getScaleLevelAttribute())->toBe('xlarge');
});
-test('get scale level attribute returns xxlarge for width greater than 1400', function () {
+test('get scale level attribute returns xxlarge for width greater than 1400', function (): void {
$deviceModel = DeviceModel::factory()->create(['width' => 1500]);
expect($deviceModel->getScaleLevelAttribute())->toBe('xxlarge');
});
-test('get scale level attribute returns null when width is null', function () {
+test('get scale level attribute returns null when width is null', function (): void {
$deviceModel = new DeviceModel(['width' => null]);
expect($deviceModel->getScaleLevelAttribute())->toBeNull();
});
-test('device model factory creates valid data', function () {
+test('device model factory creates valid data', function (): void {
$deviceModel = DeviceModel::factory()->create();
expect($deviceModel->name)->not->toBeEmpty();
diff --git a/tests/Unit/Models/PlaylistItemTest.php b/tests/Unit/Models/PlaylistItemTest.php
index 6bfe00c..428a165 100644
--- a/tests/Unit/Models/PlaylistItemTest.php
+++ b/tests/Unit/Models/PlaylistItemTest.php
@@ -4,7 +4,7 @@ use App\Models\Playlist;
use App\Models\PlaylistItem;
use App\Models\Plugin;
-test('playlist item belongs to playlist', function () {
+test('playlist item belongs to playlist', function (): void {
$playlist = Playlist::factory()->create();
$playlistItem = PlaylistItem::factory()->create(['playlist_id' => $playlist->id]);
@@ -13,7 +13,7 @@ test('playlist item belongs to playlist', function () {
->id->toBe($playlist->id);
});
-test('playlist item belongs to plugin', function () {
+test('playlist item belongs to plugin', function (): void {
$plugin = Plugin::factory()->create();
$playlistItem = PlaylistItem::factory()->create(['plugin_id' => $plugin->id]);
@@ -22,7 +22,7 @@ test('playlist item belongs to plugin', function () {
->id->toBe($plugin->id);
});
-test('playlist item can check if it is a mashup', function () {
+test('playlist item can check if it is a mashup', function (): void {
$plugin = Plugin::factory()->create();
$regularItem = PlaylistItem::factory()->create([
'mashup' => null,
@@ -44,7 +44,7 @@ test('playlist item can check if it is a mashup', function () {
->and($mashupItem->isMashup())->toBeTrue();
});
-test('playlist item can get mashup name', function () {
+test('playlist item can get mashup name', function (): void {
$plugin1 = Plugin::factory()->create();
$plugin2 = Plugin::factory()->create();
$mashupItem = PlaylistItem::factory()->create([
@@ -59,7 +59,7 @@ test('playlist item can get mashup name', function () {
expect($mashupItem->getMashupName())->toBe('Test Mashup');
});
-test('playlist item can get mashup layout type', function () {
+test('playlist item can get mashup layout type', function (): void {
$plugin1 = Plugin::factory()->create();
$plugin2 = Plugin::factory()->create();
$mashupItem = PlaylistItem::factory()->create([
@@ -74,7 +74,7 @@ test('playlist item can get mashup layout type', function () {
expect($mashupItem->getMashupLayoutType())->toBe('1Lx1R');
});
-test('playlist item can get mashup plugin ids', function () {
+test('playlist item can get mashup plugin ids', function (): void {
$plugin1 = Plugin::factory()->create();
$plugin2 = Plugin::factory()->create();
$mashupItem = PlaylistItem::factory()->create([
@@ -89,7 +89,7 @@ test('playlist item can get mashup plugin ids', function () {
expect($mashupItem->getMashupPluginIds())->toBe([$plugin1->id, $plugin2->id]);
});
-test('playlist item can get required plugin count for different layouts', function () {
+test('playlist item can get required plugin count for different layouts', function (): void {
$layouts = [
'1Lx1R' => 2,
'1Tx1B' => 2,
@@ -117,7 +117,7 @@ test('playlist item can get required plugin count for different layouts', functi
}
});
-test('playlist item can get layout type', function () {
+test('playlist item can get layout type', function (): void {
$layoutTypes = [
'1Lx1R' => 'vertical',
'1Lx2R' => 'vertical',
@@ -144,7 +144,7 @@ test('playlist item can get layout type', function () {
}
});
-test('playlist item can get layout size for different positions', function () {
+test('playlist item can get layout size for different positions', function (): void {
$plugin1 = Plugin::factory()->create();
$plugin2 = Plugin::factory()->create();
$plugin3 = Plugin::factory()->create();
@@ -163,7 +163,7 @@ test('playlist item can get layout size for different positions', function () {
->and($mashupItem->getLayoutSize(2))->toBe('half_vertical');
});
-test('playlist item can get available layouts', function () {
+test('playlist item can get available layouts', function (): void {
$layouts = PlaylistItem::getAvailableLayouts();
expect($layouts)->toBeArray()
@@ -171,7 +171,7 @@ test('playlist item can get available layouts', function () {
->and($layouts['1Lx1R'])->toBe('1 Left - 1 Right (2 plugins)');
});
-test('playlist item can get required plugin count for layout', function () {
+test('playlist item can get required plugin count for layout', function (): void {
$layouts = [
'1Lx1R' => 2,
'1Tx1B' => 2,
@@ -187,7 +187,7 @@ test('playlist item can get required plugin count for layout', function () {
}
});
-test('playlist item can create mashup', function () {
+test('playlist item can create mashup', function (): void {
$playlist = Playlist::factory()->create();
$plugins = Plugin::factory()->count(3)->create();
$pluginIds = $plugins->pluck('id')->toArray();
diff --git a/tests/Unit/Models/PlaylistTest.php b/tests/Unit/Models/PlaylistTest.php
index 55d31c7..62d3aaf 100644
--- a/tests/Unit/Models/PlaylistTest.php
+++ b/tests/Unit/Models/PlaylistTest.php
@@ -4,7 +4,7 @@ use App\Models\Device;
use App\Models\Playlist;
use App\Models\PlaylistItem;
-test('playlist has required attributes', function () {
+test('playlist has required attributes', function (): void {
$playlist = Playlist::factory()->create([
'name' => 'Test Playlist',
'is_active' => true,
@@ -21,7 +21,7 @@ test('playlist has required attributes', function () {
->active_until->format('H:i')->toBe('17:00');
});
-test('playlist belongs to device', function () {
+test('playlist belongs to device', function (): void {
$device = Device::factory()->create();
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
@@ -30,7 +30,7 @@ test('playlist belongs to device', function () {
->id->toBe($device->id);
});
-test('playlist has many items', function () {
+test('playlist has many items', function (): void {
$playlist = Playlist::factory()->create();
$items = PlaylistItem::factory()->count(3)->create(['playlist_id' => $playlist->id]);
@@ -39,7 +39,7 @@ test('playlist has many items', function () {
->each->toBeInstanceOf(PlaylistItem::class);
});
-test('getNextPlaylistItem returns null when playlist is inactive', function () {
+test('getNextPlaylistItem returns null when playlist is inactive', function (): void {
$playlist = Playlist::factory()->create(['is_active' => false]);
expect($playlist->getNextPlaylistItem())->toBeNull();
diff --git a/tests/Unit/Models/PluginTest.php b/tests/Unit/Models/PluginTest.php
index 248e6f5..ef054b1 100644
--- a/tests/Unit/Models/PluginTest.php
+++ b/tests/Unit/Models/PluginTest.php
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Http;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
-test('plugin has required attributes', function () {
+test('plugin has required attributes', function (): void {
$plugin = Plugin::factory()->create([
'name' => 'Test Plugin',
'data_payload' => ['key' => 'value'],
@@ -18,7 +18,7 @@ test('plugin has required attributes', function () {
->uuid->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/');
});
-test('plugin automatically generates uuid on creation', function () {
+test('plugin automatically generates uuid on creation', function (): void {
$plugin = Plugin::factory()->create();
expect($plugin->uuid)
@@ -26,14 +26,14 @@ test('plugin automatically generates uuid on creation', function () {
->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/');
});
-test('plugin can have custom uuid', function () {
+test('plugin can have custom uuid', function (): void {
$uuid = Illuminate\Support\Str::uuid();
$plugin = Plugin::factory()->create(['uuid' => $uuid]);
expect($plugin->uuid)->toBe($uuid);
});
-test('plugin data_payload is cast to array', function () {
+test('plugin data_payload is cast to array', function (): void {
$data = ['key' => 'value'];
$plugin = Plugin::factory()->create(['data_payload' => $data]);
@@ -42,7 +42,7 @@ test('plugin data_payload is cast to array', function () {
->toBe($data);
});
-test('plugin can have polling body for POST requests', function () {
+test('plugin can have polling body for POST requests', function (): void {
$plugin = Plugin::factory()->create([
'polling_verb' => 'post',
'polling_body' => '{"query": "query { user { id name } }"}',
@@ -51,7 +51,7 @@ test('plugin can have polling body for POST requests', function () {
expect($plugin->polling_body)->toBe('{"query": "query { user { id name } }"}');
});
-test('updateDataPayload sends POST request with body when polling_verb is post', function () {
+test('updateDataPayload sends POST request with body when polling_verb is post', function (): void {
Http::fake([
'https://example.com/api' => Http::response(['success' => true], 200),
]);
@@ -65,14 +65,12 @@ test('updateDataPayload sends POST request with body when polling_verb is post',
$plugin->updateDataPayload();
- Http::assertSent(function ($request) {
- return $request->url() === 'https://example.com/api' &&
- $request->method() === 'POST' &&
- $request->body() === '{"query": "query { user { id name } }"}';
- });
+ Http::assertSent(fn ($request): bool => $request->url() === 'https://example.com/api' &&
+ $request->method() === 'POST' &&
+ $request->body() === '{"query": "query { user { id name } }"}');
});
-test('updateDataPayload handles multiple URLs with IDX_ prefixes', function () {
+test('updateDataPayload handles multiple URLs with IDX_ prefixes', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
'polling_url' => "https://api1.example.com/data\nhttps://api2.example.com/weather\nhttps://api3.example.com/news",
@@ -99,7 +97,7 @@ test('updateDataPayload handles multiple URLs with IDX_ prefixes', function () {
expect($plugin->data_payload['IDX_2'])->toBe(['headline' => 'test']);
});
-test('updateDataPayload handles single URL without nesting', function () {
+test('updateDataPayload handles single URL without nesting', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
'polling_url' => 'https://api.example.com/data',
@@ -120,7 +118,7 @@ test('updateDataPayload handles single URL without nesting', function () {
expect($plugin->data_payload)->not->toHaveKey('IDX_0');
});
-test('updateDataPayload resolves Liquid variables in polling_header', function () {
+test('updateDataPayload resolves Liquid variables in polling_header', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
'polling_url' => 'https://api.example.com/data',
@@ -139,15 +137,13 @@ test('updateDataPayload resolves Liquid variables in polling_header', function (
$plugin->updateDataPayload();
- Http::assertSent(function ($request) {
- return $request->url() === 'https://api.example.com/data' &&
- $request->method() === 'GET' &&
- $request->header('Authorization')[0] === 'Bearer test123' &&
- $request->header('X-Custom-Header')[0] === 'custom_header_value';
- });
+ Http::assertSent(fn ($request): bool => $request->url() === 'https://api.example.com/data' &&
+ $request->method() === 'GET' &&
+ $request->header('Authorization')[0] === 'Bearer test123' &&
+ $request->header('X-Custom-Header')[0] === 'custom_header_value');
});
-test('updateDataPayload resolves Liquid variables in polling_body', function () {
+test('updateDataPayload resolves Liquid variables in polling_body', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
'polling_url' => 'https://api.example.com/data',
@@ -166,7 +162,7 @@ test('updateDataPayload resolves Liquid variables in polling_body', function ()
$plugin->updateDataPayload();
- Http::assertSent(function ($request) {
+ Http::assertSent(function ($request): bool {
$expectedBody = '{"query": "query { user { id name } }", "api_key": "test123", "user_id": "456"}';
return $request->url() === 'https://api.example.com/data' &&
@@ -175,7 +171,7 @@ test('updateDataPayload resolves Liquid variables in polling_body', function ()
});
});
-test('webhook plugin is stale if webhook event occurred', function () {
+test('webhook plugin is stale if webhook event occurred', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'webhook',
'data_payload_updated_at' => now()->subMinutes(10),
@@ -186,7 +182,7 @@ test('webhook plugin is stale if webhook event occurred', function () {
});
-test('webhook plugin data not stale if no webhook event occurred for 1 hour', function () {
+test('webhook plugin data not stale if no webhook event occurred for 1 hour', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'webhook',
'data_payload_updated_at' => now()->subMinutes(60),
@@ -197,7 +193,7 @@ test('webhook plugin data not stale if no webhook event occurred for 1 hour', fu
});
-test('plugin configuration is cast to array', function () {
+test('plugin configuration is cast to array', function (): void {
$config = ['timezone' => 'UTC', 'refresh_interval' => 30];
$plugin = Plugin::factory()->create(['configuration' => $config]);
@@ -206,7 +202,7 @@ test('plugin configuration is cast to array', function () {
->toBe($config);
});
-test('plugin can get configuration value by key', function () {
+test('plugin can get configuration value by key', function (): void {
$config = ['timezone' => 'UTC', 'refresh_interval' => 30];
$plugin = Plugin::factory()->create(['configuration' => $config]);
@@ -215,7 +211,7 @@ test('plugin can get configuration value by key', function () {
expect($plugin->getConfiguration('nonexistent', 'default'))->toBe('default');
});
-test('plugin configuration template is cast to array', function () {
+test('plugin configuration template is cast to array', function (): void {
$template = [
'custom_fields' => [
[
@@ -233,7 +229,7 @@ test('plugin configuration template is cast to array', function () {
->toBe($template);
});
-test('resolveLiquidVariables resolves variables from configuration', function () {
+test('resolveLiquidVariables resolves variables from configuration', function (): void {
$plugin = Plugin::factory()->create([
'configuration' => [
'api_key' => '12345',
@@ -263,7 +259,7 @@ test('resolveLiquidVariables resolves variables from configuration', function ()
expect($result)->toBe('High');
});
-test('resolveLiquidVariables handles invalid Liquid syntax gracefully', function () {
+test('resolveLiquidVariables handles invalid Liquid syntax gracefully', function (): void {
$plugin = Plugin::factory()->create([
'configuration' => [
'api_key' => '12345',
@@ -277,7 +273,7 @@ test('resolveLiquidVariables handles invalid Liquid syntax gracefully', function
->toThrow(Keepsuit\Liquid\Exceptions\SyntaxException::class);
});
-test('plugin can extract default values from custom fields configuration template', function () {
+test('plugin can extract default values from custom fields configuration template', function (): void {
$configurationTemplate = [
'custom_fields' => [
[
@@ -323,7 +319,7 @@ test('plugin can extract default values from custom fields configuration templat
expect($plugin->getConfiguration('timezone'))->toBeNull();
});
-test('resolveLiquidVariables resolves configuration variables correctly', function () {
+test('resolveLiquidVariables resolves configuration variables correctly', function (): void {
$plugin = Plugin::factory()->create([
'configuration' => [
'Latitude' => '48.2083',
@@ -338,7 +334,7 @@ test('resolveLiquidVariables resolves configuration variables correctly', functi
expect($plugin->resolveLiquidVariables($template))->toBe($expected);
});
-test('resolveLiquidVariables handles missing variables gracefully', function () {
+test('resolveLiquidVariables handles missing variables gracefully', function (): void {
$plugin = Plugin::factory()->create([
'configuration' => [
'Latitude' => '48.2083',
@@ -351,7 +347,7 @@ test('resolveLiquidVariables handles missing variables gracefully', function ()
expect($plugin->resolveLiquidVariables($template))->toBe($expected);
});
-test('resolveLiquidVariables handles empty configuration', function () {
+test('resolveLiquidVariables handles empty configuration', function (): void {
$plugin = Plugin::factory()->create([
'configuration' => [],
]);
diff --git a/tests/Unit/Notifications/BatteryLowTest.php b/tests/Unit/Notifications/BatteryLowTest.php
index ba53356..a809e5e 100644
--- a/tests/Unit/Notifications/BatteryLowTest.php
+++ b/tests/Unit/Notifications/BatteryLowTest.php
@@ -8,14 +8,14 @@ use App\Notifications\BatteryLow;
use App\Notifications\Channels\WebhookChannel;
use Illuminate\Notifications\Messages\MailMessage;
-test('battery low notification has correct via channels', function () {
+test('battery low notification has correct via channels', function (): void {
$device = Device::factory()->create();
$notification = new BatteryLow($device);
expect($notification->via(new User()))->toBe(['mail', WebhookChannel::class]);
});
-test('battery low notification creates correct mail message', function () {
+test('battery low notification creates correct mail message', function (): void {
$device = Device::factory()->create([
'name' => 'Test Device',
'last_battery_voltage' => 3.0,
@@ -29,7 +29,7 @@ test('battery low notification creates correct mail message', function () {
expect($mailMessage->viewData['device'])->toBe($device);
});
-test('battery low notification creates correct webhook message', function () {
+test('battery low notification creates correct webhook message', function (): void {
config([
'services.webhook.notifications.topic' => 'battery.low',
'app.name' => 'Test App',
@@ -60,7 +60,7 @@ test('battery low notification creates correct webhook message', function () {
]);
});
-test('battery low notification creates correct array representation', function () {
+test('battery low notification creates correct array representation', function (): void {
$device = Device::factory()->create([
'name' => 'Test Device',
'last_battery_voltage' => 3.0,
diff --git a/tests/Unit/Notifications/WebhookChannelTest.php b/tests/Unit/Notifications/WebhookChannelTest.php
index cdefbdd..16dbd4b 100644
--- a/tests/Unit/Notifications/WebhookChannelTest.php
+++ b/tests/Unit/Notifications/WebhookChannelTest.php
@@ -11,13 +11,13 @@ use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Response;
use Illuminate\Notifications\Notification;
-test('webhook channel returns null when no webhook url is configured', function () {
+test('webhook channel returns null when no webhook url is configured', function (): void {
$client = Mockery::mock(Client::class);
$channel = new WebhookChannel($client);
$user = new class extends User
{
- public function routeNotificationFor($driver, $notification = null)
+ public function routeNotificationFor($driver, $notification = null): null
{
return null; // No webhook URL configured
}
@@ -30,13 +30,13 @@ test('webhook channel returns null when no webhook url is configured', function
expect($result)->toBeNull();
});
-test('webhook channel throws exception when notification does not implement toWebhook', function () {
+test('webhook channel throws exception when notification does not implement toWebhook', function (): void {
$client = Mockery::mock(Client::class);
$channel = new WebhookChannel($client);
$user = new class extends User
{
- public function routeNotificationFor($driver, $notification = null)
+ public function routeNotificationFor($driver, $notification = null): string
{
return 'https://example.com/webhook';
}
@@ -44,23 +44,23 @@ test('webhook channel throws exception when notification does not implement toWe
$notification = new class extends Notification
{
- public function via($notifiable)
+ public function via($notifiable): array
{
return [];
}
};
- expect(fn () => $channel->send($user, $notification))
+ expect(fn (): ?\GuzzleHttp\Psr7\Response => $channel->send($user, $notification))
->toThrow(Exception::class, 'Notification does not implement toWebhook method.');
});
-test('webhook channel sends successful webhook request', function () {
+test('webhook channel sends successful webhook request', function (): void {
$client = Mockery::mock(Client::class);
$channel = new WebhookChannel($client);
$user = new class extends User
{
- public function routeNotificationFor($driver, $notification = null)
+ public function routeNotificationFor($driver, $notification = null): string
{
return 'https://example.com/webhook';
}
@@ -86,13 +86,13 @@ test('webhook channel sends successful webhook request', function () {
expect($result)->toBe($expectedResponse);
});
-test('webhook channel throws exception when response status is not successful', function () {
+test('webhook channel throws exception when response status is not successful', function (): void {
$client = Mockery::mock(Client::class);
$channel = new WebhookChannel($client);
$user = new class extends User
{
- public function routeNotificationFor($driver, $notification = null)
+ public function routeNotificationFor($driver, $notification = null): string
{
return 'https://example.com/webhook';
}
@@ -107,17 +107,17 @@ test('webhook channel throws exception when response status is not successful',
->once()
->andReturn($errorResponse);
- expect(fn () => $channel->send($user, $notification))
+ expect(fn (): ?\GuzzleHttp\Psr7\Response => $channel->send($user, $notification))
->toThrow(Exception::class, 'Webhook request failed with status code: 400');
});
-test('webhook channel handles guzzle exceptions', function () {
+test('webhook channel handles guzzle exceptions', function (): void {
$client = Mockery::mock(Client::class);
$channel = new WebhookChannel($client);
$user = new class extends User
{
- public function routeNotificationFor($driver, $notification = null)
+ public function routeNotificationFor($driver, $notification = null): string
{
return 'https://example.com/webhook';
}
@@ -130,6 +130,6 @@ test('webhook channel handles guzzle exceptions', function () {
->once()
->andThrow(new class extends Exception implements GuzzleException {});
- expect(fn () => $channel->send($user, $notification))
+ expect(fn (): ?\GuzzleHttp\Psr7\Response => $channel->send($user, $notification))
->toThrow(Exception::class);
});
diff --git a/tests/Unit/Notifications/WebhookMessageTest.php b/tests/Unit/Notifications/WebhookMessageTest.php
index a79f580..a6ed027 100644
--- a/tests/Unit/Notifications/WebhookMessageTest.php
+++ b/tests/Unit/Notifications/WebhookMessageTest.php
@@ -4,26 +4,26 @@ declare(strict_types=1);
use App\Notifications\Messages\WebhookMessage;
-test('webhook message can be created with static method', function () {
+test('webhook message can be created with static method', function (): void {
$message = WebhookMessage::create('test data');
expect($message)->toBeInstanceOf(WebhookMessage::class);
});
-test('webhook message can be created with constructor', function () {
+test('webhook message can be created with constructor', function (): void {
$message = new WebhookMessage('test data');
expect($message)->toBeInstanceOf(WebhookMessage::class);
});
-test('webhook message can set query parameters', function () {
+test('webhook message can set query parameters', function (): void {
$message = WebhookMessage::create()
->query(['param1' => 'value1', 'param2' => 'value2']);
expect($message->toArray()['query'])->toBe(['param1' => 'value1', 'param2' => 'value2']);
});
-test('webhook message can set data', function () {
+test('webhook message can set data', function (): void {
$data = ['key' => 'value', 'nested' => ['array' => 'data']];
$message = WebhookMessage::create()
->data($data);
@@ -31,7 +31,7 @@ test('webhook message can set data', function () {
expect($message->toArray()['data'])->toBe($data);
});
-test('webhook message can add headers', function () {
+test('webhook message can add headers', function (): void {
$message = WebhookMessage::create()
->header('X-Custom-Header', 'custom-value')
->header('Authorization', 'Bearer token');
@@ -41,7 +41,7 @@ test('webhook message can add headers', function () {
expect($headers['Authorization'])->toBe('Bearer token');
});
-test('webhook message can set user agent', function () {
+test('webhook message can set user agent', function (): void {
$message = WebhookMessage::create()
->userAgent('Test App/1.0');
@@ -49,20 +49,20 @@ test('webhook message can set user agent', function () {
expect($headers['User-Agent'])->toBe('Test App/1.0');
});
-test('webhook message can set verify option', function () {
+test('webhook message can set verify option', function (): void {
$message = WebhookMessage::create()
->verify(true);
expect($message->toArray()['verify'])->toBeTrue();
});
-test('webhook message verify defaults to false', function () {
+test('webhook message verify defaults to false', function (): void {
$message = WebhookMessage::create();
expect($message->toArray()['verify'])->toBeFalse();
});
-test('webhook message can chain methods', function () {
+test('webhook message can chain methods', function (): void {
$message = WebhookMessage::create(['initial' => 'data'])
->query(['param' => 'value'])
->data(['updated' => 'data'])
@@ -79,7 +79,7 @@ test('webhook message can chain methods', function () {
expect($array['verify'])->toBeTrue();
});
-test('webhook message toArray returns correct structure', function () {
+test('webhook message toArray returns correct structure', function (): void {
$message = WebhookMessage::create(['test' => 'data']);
$array = $message->toArray();
diff --git a/tests/Unit/Services/ImageGenerationServiceTest.php b/tests/Unit/Services/ImageGenerationServiceTest.php
index 660e984..5e3dc47 100644
--- a/tests/Unit/Services/ImageGenerationServiceTest.php
+++ b/tests/Unit/Services/ImageGenerationServiceTest.php
@@ -11,7 +11,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
-beforeEach(function () {
+beforeEach(function (): void {
TrmnlPipeline::fake();
});
@@ -37,7 +37,6 @@ it('get_image_settings returns device model settings when available', function (
// Use reflection to access private method
$reflection = new ReflectionClass(ImageGenerationService::class);
$method = $reflection->getMethod('getImageSettings');
- $method->setAccessible(true);
$settings = $method->invoke(null, $device);
@@ -66,7 +65,6 @@ it('get_image_settings falls back to device settings when no device model', func
// Use reflection to access private method
$reflection = new ReflectionClass(ImageGenerationService::class);
$method = $reflection->getMethod('getImageSettings');
- $method->setAccessible(true);
$settings = $method->invoke(null, $device);
@@ -90,7 +88,6 @@ it('get_image_settings uses defaults for missing device properties', function ()
// Use reflection to access private method
$reflection = new ReflectionClass(ImageGenerationService::class);
$method = $reflection->getMethod('getImageSettings');
- $method->setAccessible(true);
$settings = $method->invoke(null, $device);
@@ -112,7 +109,6 @@ it('determine_image_format_from_model returns correct formats', function (): voi
// Use reflection to access private method
$reflection = new ReflectionClass(ImageGenerationService::class);
$method = $reflection->getMethod('determineImageFormatFromModel');
- $method->setAccessible(true);
// Test BMP format
$bmpModel = DeviceModel::factory()->create([
diff --git a/tests/Unit/Services/OidcProviderTest.php b/tests/Unit/Services/OidcProviderTest.php
index 06da1dd..1976872 100644
--- a/tests/Unit/Services/OidcProviderTest.php
+++ b/tests/Unit/Services/OidcProviderTest.php
@@ -9,10 +9,10 @@ use GuzzleHttp\Psr7\Response;
use Illuminate\Http\Request;
use Laravel\Socialite\Two\User;
-test('oidc provider throws exception when endpoint is not configured', function () {
+test('oidc provider throws exception when endpoint is not configured', function (): void {
config(['services.oidc.endpoint' => null]);
- expect(fn () => new OidcProvider(
+ expect(fn (): OidcProvider => new OidcProvider(
new Request(),
'client-id',
'client-secret',
@@ -20,7 +20,7 @@ test('oidc provider throws exception when endpoint is not configured', function
))->toThrow(Exception::class, 'OIDC endpoint is not configured');
});
-test('oidc provider handles well-known endpoint url', function () {
+test('oidc provider handles well-known endpoint url', function (): void {
config(['services.oidc.endpoint' => 'https://example.com/.well-known/openid-configuration']);
$mockClient = Mockery::mock(Client::class);
@@ -48,7 +48,7 @@ test('oidc provider handles well-known endpoint url', function () {
expect($provider)->toBeInstanceOf(OidcProvider::class);
});
-test('oidc provider handles base url endpoint', function () {
+test('oidc provider handles base url endpoint', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -76,7 +76,7 @@ test('oidc provider handles base url endpoint', function () {
expect($provider)->toBeInstanceOf(OidcProvider::class);
});
-test('oidc provider throws exception when configuration is empty', function () {
+test('oidc provider throws exception when configuration is empty', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -90,7 +90,7 @@ test('oidc provider throws exception when configuration is empty', function () {
$this->app->instance(Client::class, $mockClient);
- expect(fn () => new OidcProvider(
+ expect(fn (): OidcProvider => new OidcProvider(
new Request(),
'client-id',
'client-secret',
@@ -98,7 +98,7 @@ test('oidc provider throws exception when configuration is empty', function () {
))->toThrow(Exception::class, 'OIDC configuration is empty or invalid JSON');
});
-test('oidc provider throws exception when authorization endpoint is missing', function () {
+test('oidc provider throws exception when authorization endpoint is missing', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -115,7 +115,7 @@ test('oidc provider throws exception when authorization endpoint is missing', fu
$this->app->instance(Client::class, $mockClient);
- expect(fn () => new OidcProvider(
+ expect(fn (): OidcProvider => new OidcProvider(
new Request(),
'client-id',
'client-secret',
@@ -123,7 +123,7 @@ test('oidc provider throws exception when authorization endpoint is missing', fu
))->toThrow(Exception::class, 'authorization_endpoint not found in OIDC configuration');
});
-test('oidc provider throws exception when configuration request fails', function () {
+test('oidc provider throws exception when configuration request fails', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -133,7 +133,7 @@ test('oidc provider throws exception when configuration request fails', function
$this->app->instance(Client::class, $mockClient);
- expect(fn () => new OidcProvider(
+ expect(fn (): OidcProvider => new OidcProvider(
new Request(),
'client-id',
'client-secret',
@@ -141,7 +141,7 @@ test('oidc provider throws exception when configuration request fails', function
))->toThrow(Exception::class, 'Failed to load OIDC configuration');
});
-test('oidc provider uses default scopes when none provided', function () {
+test('oidc provider uses default scopes when none provided', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -169,7 +169,7 @@ test('oidc provider uses default scopes when none provided', function () {
expect($provider)->toBeInstanceOf(OidcProvider::class);
});
-test('oidc provider uses custom scopes when provided', function () {
+test('oidc provider uses custom scopes when provided', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -198,7 +198,7 @@ test('oidc provider uses custom scopes when provided', function () {
expect($provider)->toBeInstanceOf(OidcProvider::class);
});
-test('oidc provider maps user data correctly', function () {
+test('oidc provider maps user data correctly', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
@@ -241,7 +241,7 @@ test('oidc provider maps user data correctly', function () {
expect($user->getAvatar())->toBe('https://example.com/avatar.jpg');
});
-test('oidc provider handles missing user fields gracefully', function () {
+test('oidc provider handles missing user fields gracefully', function (): void {
config(['services.oidc.endpoint' => 'https://example.com']);
$mockClient = Mockery::mock(Client::class);
From e4435393579cb77a14df589c501800a8f6ec12b9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 25 Sep 2025 12:50:15 +0000
Subject: [PATCH 008/132] chore(deps): bump tar-fs from 3.1.0 to 3.1.1
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.1.0 to 3.1.1.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.1.0...v3.1.1)
---
updated-dependencies:
- dependency-name: tar-fs
dependency-version: 3.1.1
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
---
package-lock.json | 60 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 57 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d434d4b..3f382af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1013,6 +1013,60 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.4.5",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.0.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.4.5",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.0.4",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.12",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.10.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.0",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.0",
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
@@ -2988,9 +3042,9 @@
}
},
"node_modules/tar-fs": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
- "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
+ "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
From 6ae3e023d41307c5f9fecb40ae4a9da62beb5116 Mon Sep 17 00:00:00 2001
From: Benjamin Nussbaum
Date: Thu, 25 Sep 2025 16:39:56 +0200
Subject: [PATCH 009/132] fix: skip view wrapper when importing blade recipes
---
app/Services/PluginImportService.php | 6 ++----
tests/Feature/PluginImportTest.php | 4 +++-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php
index 5cc928b..a9d93b3 100644
--- a/app/Services/PluginImportService.php
+++ b/app/Services/PluginImportService.php
@@ -68,12 +68,11 @@ class PluginImportService
$fullLiquid = $sharedLiquid."\n".$fullLiquid;
}
- $fullLiquid = ''."\n".$fullLiquid."\n".'
';
-
// Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
+ $fullLiquid = ''."\n".$fullLiquid."\n".'
';
}
// Ensure custom_fields is properly formatted
@@ -193,12 +192,11 @@ class PluginImportService
$fullLiquid = $sharedLiquid."\n".$fullLiquid;
}
- $fullLiquid = ''."\n".$fullLiquid."\n".'
';
-
// Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
+ $fullLiquid = ''."\n".$fullLiquid."\n".'
';
}
// Ensure custom_fields is properly formatted
diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php
index 5c4a31f..a0f3bc5 100644
--- a/tests/Feature/PluginImportTest.php
+++ b/tests/Feature/PluginImportTest.php
@@ -130,7 +130,9 @@ it('handles blade markup language correctly', function (): void {
$pluginImportService = new PluginImportService();
$plugin = $pluginImportService->importFromZip($zipFile, $user);
- expect($plugin->markup_language)->toBe('blade');
+ expect($plugin->markup_language)->toBe('blade')
+ ->and($plugin->render_markup)->not->toContain('