Compare commits
104 Commits
6fd124d0fa
...
feature/Pi
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f50f95ae9 | |||
| 5b40259058 | |||
| b091d9882b | |||
| a08ec285f6 | |||
| 13123ed961 | |||
| a8760ac3bc | |||
| 6efdbcb8be | |||
| df746b77aa | |||
| ba56e51d01 | |||
| d1b8aa9230 | |||
| 030e7a30d6 | |||
| 3378c8ead4 | |||
| e1015da044 | |||
| 4a67d7c3f1 | |||
| 0241ca1fde | |||
| fd309858cf | |||
| 8d2add8bfb | |||
| 2476e81bea | |||
| be66b16671 | |||
| 12f7a79daf | |||
| 99bb20b1e0 | |||
| dd44689f29 | |||
| 3797459037 | |||
| 0a5b383329 | |||
| f353f1d5a7 | |||
| fd8be556ec | |||
| a1c9006e77 | |||
| 40e6c00543 | |||
| 1904c353db | |||
| a8ddaff131 | |||
| b271b32bf3 | |||
| a553770fc3 | |||
| 87636453eb | |||
| 8eeb6389df | |||
| 895f7a9a9c | |||
| 67bd56d9ef | |||
| 165a89f475 | |||
| aecf3d33b4 | |||
| 1e95f8e1a5 | |||
| 474fc97629 | |||
| 701ace2f0f | |||
| d44c393614 | |||
| 6c275b9ca3 | |||
| d84efc9509 | |||
| 9d3ac40a2f | |||
| 8ca8665cda | |||
| a643301db0 | |||
| 542c256097 | |||
| 1307d97505 | |||
| ee61de4181 | |||
| 77abe5b850 | |||
| 0fb7adbf42 | |||
| 5003990575 | |||
| 9b75f67475 | |||
| 3601d03214 | |||
| 5ffecc4cdf | |||
| 3b6e4260f4 | |||
| 11bad428ad | |||
| 914dcf94da | |||
| 1f96df217a | |||
| ff9695cfff | |||
| 0eb6f5357b | |||
| 461b3e59fa | |||
| ccee999ea3 | |||
| 2ecf448b74 | |||
| c4c0364f0a | |||
| b981b629bc | |||
| 9f5cbd3d4e | |||
| 625cd8e7e3 | |||
| 98d9e43787 | |||
| 58277268e3 | |||
| 02664443b9 | |||
| 998b11b2de | |||
| 318fef7348 | |||
| 24cd276eb2 | |||
| cf2350d3a1 | |||
| ee8d64a2f6 | |||
| 3937da50a3 | |||
| 2e2d8f5df4 | |||
| 981562eb82 | |||
| 94f9ff0154 | |||
| 9d1805deee | |||
| 6111148009 | |||
| 0706abed62 | |||
| d7c34b9180 | |||
| 5e12091d95 | |||
| 5efc4ac9d7 | |||
| a00f44b4c7 | |||
| 3effc2de9b | |||
| 99fda0c88e | |||
| 0e066171b0 | |||
| bc062975f8 | |||
| 5fbeaac3e3 | |||
| ed0d01e924 | |||
| 6215f1acbe | |||
| 5c981f5c7d | |||
| b797d2a6d2 | |||
| ff0bb40028 | |||
| 8f0d2dd59e | |||
| c84923c01d | |||
| 1f9252d7e0 | |||
| ccb23dd7bf | |||
| 258cf53b4b | |||
| d1c08efd7f |
6
.idea/.idea.FA/.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.FA/.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.FA/.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.FA/.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/.idea.FA/.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
|||||||
|
<changelist name="Uncommitted_changes_before_Checkout_at_25_01_2026,_12_47_[Changes]" date="1769341635822" recycled="false" toDelete="true">
|
||||||
|
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Checkout_at_25_01_2026,_12_47_[Changes]/shelved.patch" />
|
||||||
|
<option name="DESCRIPTION" value="Uncommitted changes before Checkout at 25/01/2026, 12:47 [Changes]" />
|
||||||
|
</changelist>
|
||||||
File diff suppressed because one or more lines are too long
4
.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Update_at_24_08_2025__08_18__Changes_.xml
generated
Normal file
4
.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Update_at_24_08_2025__08_18__Changes_.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<changelist name="Uncommitted_changes_before_Update_at_24_08_2025,_08_18_[Changes]" date="1756016300553" recycled="false" toDelete="true">
|
||||||
|
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.FA/.idea/shelf/Uncommitted_changes_before_Update_at_24_08_2025,_08_18_[Changes]/shelved.patch" />
|
||||||
|
<option name="DESCRIPTION" value="Uncommitted changes before Update at 24/08/2025, 08:18 [Changes]" />
|
||||||
|
</changelist>
|
||||||
@@ -124,11 +124,15 @@ steps:
|
|||||||
dotnet test --configuration Release --logger "trx;LogFileName=tests.trx" --results-directory TestResults
|
dotnet test --configuration Release --logger "trx;LogFileName=tests.trx" --results-directory TestResults
|
||||||
done < changed-projects.txt
|
done < changed-projects.txt
|
||||||
depends_on: [restore]
|
depends_on: [restore]
|
||||||
ai-agent-gate:
|
pipeline-agent:
|
||||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
environment:
|
environment:
|
||||||
OPENAI_API_KEY:
|
OPENAI_API_KEY:
|
||||||
from_secret: openai_api_key
|
from_secret: openai_api_key
|
||||||
|
VAULT_URL:
|
||||||
|
from_secret: vault_url
|
||||||
|
VAULT_TOKEN:
|
||||||
|
from_secret: vault_token
|
||||||
commands:
|
commands:
|
||||||
- |
|
- |
|
||||||
set -euf
|
set -euf
|
||||||
@@ -139,14 +143,8 @@ steps:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== AI-Gate: analiza wyników testów: ==="
|
dotnet run --project PipelineAgent/PipelineAgent.csproj "check-changes" "$(pwd)"
|
||||||
if [ ! -f "TestResults/tests.trx" ]; then
|
depends_on: [detect-changed-projects]
|
||||||
echo "Brak pliku TestResults/tests.trx – blokuję pipeline."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet run --project PipelineAgent/PipelineAgent.csproj "test-gate" "TestResults/tests.trx"
|
|
||||||
depends_on: [test]
|
|
||||||
pack-datamodels:
|
pack-datamodels:
|
||||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
commands:
|
commands:
|
||||||
@@ -195,7 +193,8 @@ steps:
|
|||||||
done < changed-projects.txt
|
done < changed-projects.txt
|
||||||
echo "=== Spakowane pakiety ==="
|
echo "=== Spakowane pakiety ==="
|
||||||
ls -la nupkg/ || echo "Brak spakowanych pakietów!"
|
ls -la nupkg/ || echo "Brak spakowanych pakietów!"
|
||||||
depends_on: [ai-agent-gate]
|
#depends_on: [ai-agent-gate]
|
||||||
|
depends_on: [test]
|
||||||
|
|
||||||
publish-linux-local:
|
publish-linux-local:
|
||||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
@@ -248,7 +247,8 @@ steps:
|
|||||||
else
|
else
|
||||||
echo "Brak aplikacji LinuxLocal do publikacji"
|
echo "Brak aplikacji LinuxLocal do publikacji"
|
||||||
fi
|
fi
|
||||||
depends_on: [ai-agent-gate]
|
#depends_on: [ai-agent-gate]
|
||||||
|
depends_on: [test]
|
||||||
|
|
||||||
publish-datamodels-to-baget:
|
publish-datamodels-to-baget:
|
||||||
image: mcr.microsoft.com/dotnet/sdk:latest
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
|||||||
491
.woodpecker/merge_to_master.yml
Normal file
491
.woodpecker/merge_to_master.yml
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
skip_clone: true
|
||||||
|
when:
|
||||||
|
event: pull_request
|
||||||
|
action:
|
||||||
|
- merge
|
||||||
|
branch: master
|
||||||
|
steps:
|
||||||
|
clone-manual:
|
||||||
|
image: woodpeckerci/plugin-git
|
||||||
|
settings:
|
||||||
|
remote: http://gitea:3000/${CI_REPO}.git
|
||||||
|
branch: ${CI_COMMIT_BRANCH}
|
||||||
|
depth: 1
|
||||||
|
detect-changed-projects:
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
echo "CI_PIPELINE_FILES=${CI_PIPELINE_FILES:-<empty>}"
|
||||||
|
|
||||||
|
apk add --no-cache findutils
|
||||||
|
|
||||||
|
> changed-projects.txt
|
||||||
|
|
||||||
|
echo "${CI_PIPELINE_FILES:-}" \
|
||||||
|
| sed 's/^\[//; s/\]$//' \
|
||||||
|
| tr ',' '\n' \
|
||||||
|
| sed 's/^ *//; s/ *$//' \
|
||||||
|
| sed '/^$/d' \
|
||||||
|
| while read path; do
|
||||||
|
echo "→ changed file: $path"
|
||||||
|
|
||||||
|
dir=$(dirname "$path")
|
||||||
|
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
|
||||||
|
csproj=$(find "$dir" -maxdepth 1 -name "*.csproj" -type f | head -n 1 || true)
|
||||||
|
if [ -n "$csproj" ]; then
|
||||||
|
echo "$csproj" >> changed-projects.txt
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
dir=$(dirname "$dir")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# unikalne projekty
|
||||||
|
sort -u -o changed-projects.txt changed-projects.txt
|
||||||
|
|
||||||
|
echo "Zmienione projekty:"
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pipeline zakonczony."
|
||||||
|
# exit 0 = sukces, wiec kolejne kroki, ktore od niego zaleza, nie poleca
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
cat changed-projects.txt
|
||||||
|
depends_on: [clone-manual]
|
||||||
|
debug-location:
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- pwd
|
||||||
|
- ls -la
|
||||||
|
- find /woodpecker -maxdepth 4 -type d 2>/dev/null
|
||||||
|
depends_on: [detect-changed-projects]
|
||||||
|
restore:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
echo "Aktualna ścieżka do pliku: $(pwd)"
|
||||||
|
echo "=== Restore wszystkich projektów ==="
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektów – nic do restore."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF > NuGet.Config
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== Restore zmienionych projektów ==="
|
||||||
|
while read csproj; do
|
||||||
|
echo "→ dotnet restore $csproj"
|
||||||
|
dotnet restore "$csproj"
|
||||||
|
done < changed-projects.txt
|
||||||
|
|
||||||
|
# find . -name "*.csproj" -type f -exec dotnet restore "{}" \;
|
||||||
|
depends_on: [detect-changed-projects]
|
||||||
|
test:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam test."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Konfiguracja NuGet sources ==="
|
||||||
|
cat <<EOF > NuGet.Config
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== Uruchamianie testów dla zmienionych projektow ==="
|
||||||
|
while read csproj; do
|
||||||
|
echo "→ dotnet test $csproj"
|
||||||
|
dotnet test --configuration Release --logger "trx;LogFileName=tests.trx" --results-directory TestResults
|
||||||
|
done < changed-projects.txt
|
||||||
|
depends_on: [restore]
|
||||||
|
pack-datamodels:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam pack-datamodels."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Instalacja MinVer CLI ==="
|
||||||
|
dotnet tool install --tool-path /tmp/minver minver-cli
|
||||||
|
echo "=== Obliczona wersja przez MinVer ==="
|
||||||
|
MINVER_VERSION=$(/tmp/minver/minver \
|
||||||
|
--auto-increment patch \
|
||||||
|
--minimum-major-minor 2.0 \
|
||||||
|
--default-pre-release-identifiers alpha.0 \
|
||||||
|
--verbosity info)
|
||||||
|
echo "Wersja: $MINVER_VERSION"
|
||||||
|
mkdir -p nupkg
|
||||||
|
echo "=== Pełniejsze fetch git (unshallow + tags) ==="
|
||||||
|
git fetch --prune --unshallow || echo "Już full clone – OK"
|
||||||
|
git fetch --tags
|
||||||
|
echo "=== Dostępne tagi ==="
|
||||||
|
git tag -l
|
||||||
|
echo "=== Aktualny commit i opis ==="
|
||||||
|
git describe --tags --always --dirty
|
||||||
|
echo "=== Diagnostyka projektów ==="
|
||||||
|
find . -name "*.csproj" -type f | sort
|
||||||
|
mkdir -p nupkg
|
||||||
|
echo "=== Pakowanie projektów DataModel (wykrywane po obecności <PackageId>) ==="
|
||||||
|
while read csproj; do
|
||||||
|
if grep -q '<PackageId>' "$csproj"; then
|
||||||
|
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||||
|
echo "→ Pakuję $PROJECT_NAME ($csproj)"
|
||||||
|
dotnet pack "$csproj" \
|
||||||
|
--configuration Release \
|
||||||
|
-o "./nupkg" \
|
||||||
|
/p:PackageVersion=$MINVER_VERSION
|
||||||
|
else
|
||||||
|
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||||
|
echo "→ Pomijam $PROJECT_NAME – brak <PackageId>"
|
||||||
|
fi
|
||||||
|
done < changed-projects.txt
|
||||||
|
echo "=== Spakowane pakiety ==="
|
||||||
|
ls -la nupkg/ || echo "Brak spakowanych pakietów!"
|
||||||
|
depends_on: [test]
|
||||||
|
|
||||||
|
publish-linux-local:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam AI-gate."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p linux-apps
|
||||||
|
|
||||||
|
echo "=== Konfiguracja NuGet sources ==="
|
||||||
|
cat <<EOF > NuGet.Config
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== Publikacja aplikacji LinuxLocal ==="
|
||||||
|
#find . -name "*.csproj" -type f | while read csproj; do
|
||||||
|
while read csproj; do
|
||||||
|
PROJECT_TYPE=$(dotnet msbuild "$csproj" -getProperty:ProjectType -noLogo 2>/dev/null || echo "Unknown")
|
||||||
|
|
||||||
|
if [ "$PROJECT_TYPE" = "LinuxLocal" ]; then
|
||||||
|
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||||
|
echo "→ Publishing $PROJECT_NAME"
|
||||||
|
|
||||||
|
dotnet publish "$csproj" \
|
||||||
|
--configuration Release \
|
||||||
|
--output "./linux-apps/$PROJECT_NAME" \
|
||||||
|
--runtime linux-x64 \
|
||||||
|
--self-contained false
|
||||||
|
fi
|
||||||
|
#done
|
||||||
|
done < changed-projects.txt
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ -d "./linux-apps" ] && [ "$(ls -A ./linux-apps)" ]; then
|
||||||
|
echo "Opublikowane aplikacje:"
|
||||||
|
ls -la linux-apps/
|
||||||
|
else
|
||||||
|
echo "Brak aplikacji LinuxLocal do publikacji"
|
||||||
|
fi
|
||||||
|
depends_on: [test]
|
||||||
|
|
||||||
|
publish-datamodels-to-baget:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
environment:
|
||||||
|
BAGETTER_API_KEY:
|
||||||
|
from_secret: baget_api_key
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam publish-datamodels-to-baget."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Test połączenia z BaGetter ==="
|
||||||
|
curl -f http://baget:80/v3/index.json || echo "Nie można połączyć się z BaGetter!"
|
||||||
|
cat <<EOF > NuGet.Config
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
echo "=== Publikacja pakietów do BaGetter ==="
|
||||||
|
find ./nupkg -name "*.nupkg" -type f | while read pkg; do
|
||||||
|
echo "→ Push $(basename "$pkg")"
|
||||||
|
dotnet nuget push "$pkg" \
|
||||||
|
--source "BaGet" \
|
||||||
|
--api-key "$BAGETTER_API_KEY" \
|
||||||
|
--skip-duplicate
|
||||||
|
done
|
||||||
|
depends_on: [pack-datamodels]
|
||||||
|
|
||||||
|
select-projects-for-container:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
> projects-to-build.txt
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam select-projects-for-container."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Selekcja projektów do konteneryzacji ==="
|
||||||
|
#find . -name "*.csproj" -type f | while read csproj; do
|
||||||
|
while read csproj; do
|
||||||
|
PROJECT_NAME=$(basename "$csproj" .csproj)
|
||||||
|
PROJECT_TYPE=$(dotnet msbuild "$csproj" -getProperty:ProjectType -noLogo 2>/dev/null || echo "Unknown")
|
||||||
|
|
||||||
|
if [ "$PROJECT_TYPE" = "Linux" ] || [ "$PROJECT_TYPE" = "Api" ] || [ "$PROJECT_TYPE" = "ServerProcess" ] || [ "$PROJECT_TYPE" = "Blazor" ] || [ "$PROJECT_TYPE" = "LinuxLocal" ]; then
|
||||||
|
PROJECT_DIR=$(dirname "$csproj")
|
||||||
|
echo "→ Dodaję $PROJECT_NAME (typ: $PROJECT_TYPE)"
|
||||||
|
echo "$PROJECT_DIR|$PROJECT_NAME" >> projects-to-build.txt
|
||||||
|
else
|
||||||
|
echo "→ Pomijam $PROJECT_NAME (typ: $PROJECT_TYPE)"
|
||||||
|
fi
|
||||||
|
#done
|
||||||
|
done < changed-projects.txt
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ -s projects-to-build.txt ]; then
|
||||||
|
echo "Znalezione projekty:"
|
||||||
|
cat projects-to-build.txt
|
||||||
|
echo "Łącznie: $(wc -l < projects-to-build.txt)"
|
||||||
|
else
|
||||||
|
echo "BRAK PROJEKTÓW DO BUDOWY!"
|
||||||
|
fi
|
||||||
|
depends_on: [publish-datamodels-to-baget]
|
||||||
|
|
||||||
|
containerize-apps:
|
||||||
|
image: docker:27-cli
|
||||||
|
environment:
|
||||||
|
GITEA_USER:
|
||||||
|
from_secret: gitea_registry_user
|
||||||
|
GITEA_TOKEN:
|
||||||
|
from_secret: gitea_registry_token
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam contenerize-apps."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REGISTRY_ORG="fa"
|
||||||
|
|
||||||
|
echo "=== DEBUG: Zawartość projects-to-build.txt ==="
|
||||||
|
cat projects-to-build.txt || echo "Plik nie istnieje!"
|
||||||
|
echo "=== DEBUG: REGISTRY_ORG = '$REGISTRY_ORG' ==="
|
||||||
|
|
||||||
|
echo "$GITEA_TOKEN" | docker login git.modwad.pl -u "$GITEA_USER" --password-stdin
|
||||||
|
|
||||||
|
if [ ! -s projects-to-build.txt ]; then
|
||||||
|
echo "Brak projektow"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f NuGet.Config
|
||||||
|
|
||||||
|
cat > NuGet.Config <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="BaGet" value="http://127.0.0.1:8080/v3/index.json" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat projects-to-build.txt | while IFS='|' read -r PDIR PNAME; do
|
||||||
|
PDIR=$(echo "$PDIR" | xargs)
|
||||||
|
PNAME=$(echo "$PNAME" | xargs)
|
||||||
|
|
||||||
|
echo "=== DEBUG: PDIR='$PDIR' PNAME='$PNAME' ==="
|
||||||
|
|
||||||
|
test -z "$PNAME" && continue
|
||||||
|
test ! -d "$PDIR" && continue
|
||||||
|
|
||||||
|
echo -n "git.modwad.pl/fa/" > /tmp/tag.txt
|
||||||
|
echo "$PNAME" | tr 'A-Z' 'a-z' | tr -d '\n' >> /tmp/tag.txt
|
||||||
|
|
||||||
|
echo "=== DEBUG: Generated tag: $(cat /tmp/tag.txt) ==="
|
||||||
|
|
||||||
|
echo "$PNAME" > /tmp/pname.txt
|
||||||
|
echo "$PDIR" | sed 's|^\./||' > /tmp/pdir.txt
|
||||||
|
|
||||||
|
cat > "Dockerfile.temp" <<'ENDOFDOCKERFILE'
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet restore "%%PROJECTDIR%%/%%PROJECTNAME%%.csproj"
|
||||||
|
RUN dotnet publish "%%PROJECTDIR%%/%%PROJECTNAME%%.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
ENTRYPOINT ["dotnet", "%%PROJECTNAME%%.dll"]
|
||||||
|
ENDOFDOCKERFILE
|
||||||
|
|
||||||
|
REAL_PNAME=$(cat /tmp/pname.txt)
|
||||||
|
REAL_PDIR=$(cat /tmp/pdir.txt)
|
||||||
|
sed "s|%%PROJECTNAME%%|$REAL_PNAME|g; s|%%PROJECTDIR%%|$REAL_PDIR|g" "Dockerfile.temp" > "Dockerfile.final"
|
||||||
|
|
||||||
|
echo "=== Budowanie obrazu ==="
|
||||||
|
DOCKER_BUILDKIT=0 docker build \
|
||||||
|
--network=host \
|
||||||
|
-t "$(cat /tmp/tag.txt):${CI_COMMIT_SHA}" \
|
||||||
|
-t "$(cat /tmp/tag.txt):latest" \
|
||||||
|
-f "Dockerfile.final" \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo "=== Pushing SHA tag ==="
|
||||||
|
docker push "$(cat /tmp/tag.txt):${CI_COMMIT_SHA}"
|
||||||
|
|
||||||
|
echo "=== Pushing latest tag ==="
|
||||||
|
docker push "$(cat /tmp/tag.txt):latest"
|
||||||
|
|
||||||
|
echo "=== SUCCESS: $(cat /tmp/tag.txt) ==="
|
||||||
|
|
||||||
|
rm -f "Dockerfile.temp" "Dockerfile.final"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== WSZYSTKIE PROJEKTY ZAKONCZONE ==="
|
||||||
|
depends_on: [select-projects-for-container]
|
||||||
|
|
||||||
|
deploy-linux-local:
|
||||||
|
image: alpine:latest
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam deploy-linux-local."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Sprawdzanie aplikacji LinuxLocal ==="
|
||||||
|
if [ ! -d "./linux-apps" ]; then
|
||||||
|
echo "Folder ./linux-apps nie istnieje"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== DEBUG: Zawartość ./linux-apps/ ==="
|
||||||
|
ls -la ./linux-apps/
|
||||||
|
|
||||||
|
echo "=== Wdrażanie aplikacji LinuxLocal do /opt/fa-apps/ ==="
|
||||||
|
mkdir -p /opt/fa-apps
|
||||||
|
|
||||||
|
cd ./linux-apps
|
||||||
|
|
||||||
|
for app in *; do
|
||||||
|
echo "→ DEBUG: Sprawdzam $app"
|
||||||
|
|
||||||
|
if [ ! -d "$app" ]; then
|
||||||
|
echo " → $app nie jest folderem, pomijam"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " → Kopiuję $app"
|
||||||
|
echo " → Zawartość:"
|
||||||
|
ls -la "$app/"
|
||||||
|
|
||||||
|
rm -rf "/opt/fa-apps/$app"
|
||||||
|
cp -r "$app" "/opt/fa-apps/$app"
|
||||||
|
|
||||||
|
find "/opt/fa-apps/$app" -type f -executable -exec chmod +x {} \;
|
||||||
|
|
||||||
|
echo " → Skopiowano!"
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Wdrożone aplikacje ==="
|
||||||
|
ls -laR /opt/fa-apps/
|
||||||
|
depends_on: [publish-linux-local, containerize-apps]
|
||||||
|
|
||||||
|
cleanup-docker:
|
||||||
|
image: docker:27-cli
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
echo "=== Status przed czyszczeniem ==="
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Usuwanie dangling images ==="
|
||||||
|
docker image prune -f
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Usuwanie starych wersji aplikacji ==="
|
||||||
|
docker images --format "{{.Repository}}:{{.Tag}}" | grep "git.modwad.pl/" | grep -v ":latest" | grep -v ":${CI_COMMIT_SHA}" | while read img; do
|
||||||
|
echo "Usuwam: $img"
|
||||||
|
docker rmi "$img" || true
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Usuwanie kontenerów ==="
|
||||||
|
docker container prune -f
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Usuwanie build cache ==="
|
||||||
|
docker builder prune -af
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Status po czyszczeniu ==="
|
||||||
|
docker system df
|
||||||
|
depends_on: [deploy-linux-local]
|
||||||
|
when:
|
||||||
|
status: [success, failure]
|
||||||
89
.woodpecker/push_to_repo.yml
Normal file
89
.woodpecker/push_to_repo.yml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
skip_clone: true
|
||||||
|
when:
|
||||||
|
event: push
|
||||||
|
branch:
|
||||||
|
- feature/*
|
||||||
|
|
||||||
|
steps:
|
||||||
|
clone-manual:
|
||||||
|
image: woodpeckerci/plugin-git
|
||||||
|
settings:
|
||||||
|
remote: http://gitea:3000/${CI_REPO}.git
|
||||||
|
branch: ${CI_COMMIT_BRANCH}
|
||||||
|
depth: 1
|
||||||
|
detect-changed-projects:
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
echo "CI_PIPELINE_FILES=${CI_PIPELINE_FILES:-<empty>}"
|
||||||
|
|
||||||
|
apk add --no-cache findutils
|
||||||
|
|
||||||
|
> changed-projects.txt
|
||||||
|
|
||||||
|
echo "${CI_PIPELINE_FILES:-}" \
|
||||||
|
| sed 's/^\[//; s/\]$//' \
|
||||||
|
| tr ',' '\n' \
|
||||||
|
| sed 's/^ *//; s/ *$//' \
|
||||||
|
| sed '/^$/d' \
|
||||||
|
| while read path; do
|
||||||
|
echo "→ changed file: $path"
|
||||||
|
|
||||||
|
dir=$(dirname "$path")
|
||||||
|
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
|
||||||
|
csproj=$(find "$dir" -maxdepth 1 -name "*.csproj" -type f | head -n 1 || true)
|
||||||
|
if [ -n "$csproj" ]; then
|
||||||
|
echo "$csproj" >> changed-projects.txt
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
dir=$(dirname "$dir")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# unikalne projekty
|
||||||
|
sort -u -o changed-projects.txt changed-projects.txt
|
||||||
|
|
||||||
|
echo "Zmienione projekty:"
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pipeline zakonczony."
|
||||||
|
# exit 0 = sukces, wiec kolejne kroki, ktore od niego zaleza, nie poleca
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
cat changed-projects.txt
|
||||||
|
depends_on: [clone-manual]
|
||||||
|
pipeline-agent:
|
||||||
|
image: mcr.microsoft.com/dotnet/sdk:latest
|
||||||
|
environment:
|
||||||
|
OPENAI_API_KEY:
|
||||||
|
from_secret: openai_api_key
|
||||||
|
VAULT_URL:
|
||||||
|
from_secret: vault_url
|
||||||
|
VAULT_TOKEN:
|
||||||
|
from_secret: vault_token
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
set -euf
|
||||||
|
cd "${CI_WORKSPACE}"
|
||||||
|
|
||||||
|
echo "=== Konfiguracja NuGet sources ==="
|
||||||
|
cat <<EOF > NuGet.Config
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||||
|
<add key="BaGet" value="http://baget:80/v3/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
</configuration>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ ! -s changed-projects.txt ]; then
|
||||||
|
echo "Brak zmienionych projektow – pomijam AI-gate."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
dotnet run --project PipelineAgent/PipelineAgent.csproj "check-changes" "$(pwd)"
|
||||||
|
depends_on: [detect-changed-projects]
|
||||||
47
BroseCumulativeReport.Tests/AppTests.cs
Normal file
47
BroseCumulativeReport.Tests/AppTests.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using FaKrosnoEfDataModel.Services;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using SytelineSaAppEfDataModel.Services;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests
|
||||||
|
{
|
||||||
|
public class AppTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IScheduleOrderService> _mockScheduleOrderService = new();
|
||||||
|
private readonly Mock<IScheduleOrderDetailsService> _mockScheduleOrderDetailsService = new();
|
||||||
|
private readonly Mock<ICustomerOrderService> _mockCustomerOrderService = new();
|
||||||
|
private readonly Mock<IMaterialTransactionService> _mockMaterialTransactionService = new();
|
||||||
|
private readonly Mock<IConfiguration> _mockConfiguration = new();
|
||||||
|
|
||||||
|
private App CreateApp()
|
||||||
|
{
|
||||||
|
return new App(
|
||||||
|
_mockScheduleOrderService.Object,
|
||||||
|
_mockScheduleOrderDetailsService.Object,
|
||||||
|
_mockCustomerOrderService.Object,
|
||||||
|
_mockMaterialTransactionService.Object,
|
||||||
|
_mockConfiguration.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClosestSendingDate_ShouldReturnNextThursday_WhenCalledWithVariousDates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
DateTime friday = new DateTime(2023, 1, 6); // Friday
|
||||||
|
DateTime thursday = new DateTime(2023, 1, 5); // Thursday
|
||||||
|
DateTime wednesday = new DateTime(2023,1,4); // Wednesday
|
||||||
|
|
||||||
|
// Act
|
||||||
|
DateTime resultFriday = App.GetClosestSendingDate(friday);
|
||||||
|
DateTime resultThursday = App.GetClosestSendingDate(thursday);
|
||||||
|
DateTime resultWednesday = App.GetClosestSendingDate(wednesday);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new DateTime(2023, 1, 12), resultFriday); // Next Thursday
|
||||||
|
Assert.Equal(new DateTime(2023, 1, 12), resultThursday); // Next Thursday
|
||||||
|
Assert.Equal(new DateTime(2023, 1, 5), resultWednesday); // This Thursday
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
68
BroseCumulativeReport.Tests/ExcelGeneratorTests.cs
Normal file
68
BroseCumulativeReport.Tests/ExcelGeneratorTests.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using BroseCumulativeReport;
|
||||||
|
using BroseCumulativeReport.Model;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests
|
||||||
|
{
|
||||||
|
public class ExcelGeneratorTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IConfiguration> _mockConfiguration = new();
|
||||||
|
|
||||||
|
public ExcelGeneratorTests()
|
||||||
|
{
|
||||||
|
// Setup mock IConfiguration behavior if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateExcel_ShouldCreateFile_WhenProvidedValidInput()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var excelGenerator = new ExcelGenerator(_mockConfiguration.Object);
|
||||||
|
string tempFilePath = Path.GetTempFileName();
|
||||||
|
List<MainRow> mainRows = new List<MainRow>();
|
||||||
|
|
||||||
|
// Minimal MainRow data to avoid empty file scenario
|
||||||
|
mainRows.Add(new MainRow
|
||||||
|
{
|
||||||
|
CustomerName = "Customer",
|
||||||
|
CustomerOrderNumber = "Order1",
|
||||||
|
OrderNumber = "OrderNum",
|
||||||
|
OrderDate = DateTime.Today,
|
||||||
|
SalesChannelProductCode = "SCP",
|
||||||
|
ShipperProductCode = "SPC",
|
||||||
|
LastWzNumber = "WZ123",
|
||||||
|
WzQuantity = 1,
|
||||||
|
SendDatesWithQuantity = new List<(DateTime, int)> { (DateTime.Today, 1) },
|
||||||
|
ChildRows = new List<ChildRow>()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
excelGenerator.GenerateExcel(tempFilePath, mainRows);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(File.Exists(tempFilePath));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
//[Fact]
|
||||||
|
// This test requires refactoring ExcelGenerator to allow injection of SMTP client
|
||||||
|
// to mock and verify sending logic without side effects.
|
||||||
|
//public void SendEmail_ShouldSendEmailWithoutExceptions_WhenCalled()
|
||||||
|
//{
|
||||||
|
// // Arrange
|
||||||
|
// var excelGenerator = new ExcelGenerator(_mockConfiguration.Object);
|
||||||
|
// string dummyFilePath = "dummyfile.xlsx";
|
||||||
|
|
||||||
|
// // Act & Assert
|
||||||
|
// var exception = Record.Exception(() => excelGenerator.SendEmail(dummyFilePath));
|
||||||
|
// Assert.Null(exception);
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
BroseCumulativeReport.Tests/IAppTests.cs
Normal file
24
BroseCumulativeReport.Tests/IAppTests.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using BroseCumulativeReport;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests
|
||||||
|
{
|
||||||
|
public class IAppTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_ShouldBeAwaitable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockApp = new Mock<IApp>();
|
||||||
|
mockApp.Setup(app => app.RunAsync()).Returns(Task.CompletedTask).Verifiable();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await mockApp.Object.RunAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockApp.Verify(v => v.RunAsync(), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
BroseCumulativeReport.Tests/Model/ChildRowTests.cs
Normal file
26
BroseCumulativeReport.Tests/Model/ChildRowTests.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using BroseCumulativeReport.Model;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests.Model
|
||||||
|
{
|
||||||
|
public class ChildRowTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string wzNumber = "WZ1";
|
||||||
|
decimal quantity = 5.5m;
|
||||||
|
DateTime date = DateTime.Today;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var childRow = new ChildRow(wzNumber, quantity, date);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(wzNumber, childRow.WzNumber);
|
||||||
|
Assert.Equal(quantity, childRow.Quantity);
|
||||||
|
Assert.Equal(date, childRow.Date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
BroseCumulativeReport.Tests/Model/ItemTests.cs
Normal file
30
BroseCumulativeReport.Tests/Model/ItemTests.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using BroseCumulativeReport.Model;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests.Model
|
||||||
|
{
|
||||||
|
public class ItemTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string customerName = "Customer";
|
||||||
|
string salesCode = "SCP";
|
||||||
|
string shipperCode = "SPC";
|
||||||
|
int quantity = 10;
|
||||||
|
string qtyType = "Type";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var item = new Item(customerName, salesCode, shipperCode, quantity, qtyType);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(customerName, item.CustomerName);
|
||||||
|
Assert.Equal(salesCode, item.SalesChannelProductCode);
|
||||||
|
Assert.Equal(shipperCode, item.ShipperProductCode);
|
||||||
|
Assert.Equal(quantity, item.Quantity);
|
||||||
|
Assert.Equal(qtyType, item.QuantityType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
BroseCumulativeReport.Tests/Model/MainRowTests.cs
Normal file
38
BroseCumulativeReport.Tests/Model/MainRowTests.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BroseCumulativeReport.Model;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests.Model
|
||||||
|
{
|
||||||
|
public class MainRowTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Properties_ShouldSetAndGetCorrectly()
|
||||||
|
{
|
||||||
|
var mainRow = new MainRow
|
||||||
|
{
|
||||||
|
CustomerName = "Customer",
|
||||||
|
CustomerOrderNumber = "OrderNumber",
|
||||||
|
OrderNumber = "Order123",
|
||||||
|
OrderDate = DateTime.Now,
|
||||||
|
SalesChannelProductCode = "SCP",
|
||||||
|
ShipperProductCode = "SPC",
|
||||||
|
LastWzNumber = "WZ123",
|
||||||
|
WzQuantity = 10,
|
||||||
|
SendDatesWithQuantity = new List<(DateTime, int)> { (DateTime.Today, 1) },
|
||||||
|
ChildRows = new List<ChildRow>()
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal("Customer", mainRow.CustomerName);
|
||||||
|
Assert.Equal("OrderNumber", mainRow.CustomerOrderNumber);
|
||||||
|
Assert.Equal("Order123", mainRow.OrderNumber);
|
||||||
|
Assert.Equal("SCP", mainRow.SalesChannelProductCode);
|
||||||
|
Assert.Equal("SPC", mainRow.ShipperProductCode);
|
||||||
|
Assert.Equal("WZ123", mainRow.LastWzNumber);
|
||||||
|
Assert.Equal(10, mainRow.WzQuantity);
|
||||||
|
Assert.Single(mainRow.SendDatesWithQuantity);
|
||||||
|
Assert.Empty(mainRow.ChildRows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
BroseCumulativeReport.Tests/Model/WzTests.cs
Normal file
33
BroseCumulativeReport.Tests/Model/WzTests.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using BroseCumulativeReport.Model;
|
||||||
|
using FaKrosnoEfDataModel.Dtos;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests.Model
|
||||||
|
{
|
||||||
|
public class WzTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_ShouldSetPropertiesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string poNumber = "PO123";
|
||||||
|
string wzNumberInput = "WZ_456";
|
||||||
|
string expectedWzNumber = "WZ456";
|
||||||
|
string custOrderNumber = "CO789";
|
||||||
|
var scheduleOrder = new ScheduleOrderDto(); // Assuming default constructor available
|
||||||
|
int quantity = 42;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var wz = new Wz(poNumber, wzNumberInput, custOrderNumber, scheduleOrder, quantity);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(poNumber, wz.PoNumber);
|
||||||
|
// Verify that underscores are removed as expected
|
||||||
|
Assert.Equal(expectedWzNumber, wz.WzNumber);
|
||||||
|
Assert.Equal(custOrderNumber, wz.CustomerOrderNumber);
|
||||||
|
Assert.Equal(scheduleOrder, wz.ScheduleOrder);
|
||||||
|
Assert.Equal(quantity, wz.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
BroseCumulativeReport.Tests/ServiceTests.cs
Normal file
25
BroseCumulativeReport.Tests/ServiceTests.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using BroseCumulativeReport;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BroseCumulativeReport.Tests
|
||||||
|
{
|
||||||
|
public class ServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CreateHostBuilder_ShouldReturnHostBuilder_WithExpectedConfiguration()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var service = new Service();
|
||||||
|
string[] args = Array.Empty<string>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
IHostBuilder hostBuilder = service.CreateHostBuilder(args);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(hostBuilder);
|
||||||
|
// TODO: Additional asserts to verify configuration and service setup if possible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,8 +54,6 @@ public class App(
|
|||||||
scheduleOrder.ScheduleOrderDetails.FirstOrDefault()?.ScheduleOrderDetailMiscs
|
scheduleOrder.ScheduleOrderDetails.FirstOrDefault()?.ScheduleOrderDetailMiscs
|
||||||
?.FirstOrDefault(x => x.Type == "AAK")?.Value ?? string.Empty;
|
?.FirstOrDefault(x => x.Type == "AAK")?.Value ?? string.Empty;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(wzNumber)) continue;
|
|
||||||
|
|
||||||
int quantity = int.Parse(scheduleOrder.ScheduleOrderDetails.FirstOrDefault()?.ScheduleOrderDetailMiscs
|
int quantity = int.Parse(scheduleOrder.ScheduleOrderDetails.FirstOrDefault()?.ScheduleOrderDetailMiscs
|
||||||
?.FirstOrDefault(x => x.Type is "48" or "70")?.Value ?? "0");
|
?.FirstOrDefault(x => x.Type is "48" or "70")?.Value ?? "0");
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"SA/24/2053",
|
"SA/24/2053",
|
||||||
"SA/25/0988",
|
"SA/25/0988",
|
||||||
"SA/25/2291",
|
"SA/25/2291",
|
||||||
"SA/25/1415"
|
"SA/25/1415",
|
||||||
|
"SA/26/0692"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,18 +28,7 @@ public class ClientBrose(
|
|||||||
{
|
{
|
||||||
public new async Task<EdiCustomerOrderDto?> ProcessClientOrder(OrderModel orderModel)
|
public new async Task<EdiCustomerOrderDto?> ProcessClientOrder(OrderModel orderModel)
|
||||||
{
|
{
|
||||||
EdiCustomerOrderDto? ediCustomerOrderDto = await base.ProcessClientOrder(orderModel);
|
return await base.ProcessClientOrder(orderModel);
|
||||||
|
|
||||||
string wzNumber =
|
|
||||||
orderModel.ScheduleOrderDto.ScheduleOrderDetails.FirstOrDefault()?.ScheduleOrderDetailMiscs
|
|
||||||
?.FirstOrDefault(x => x.Type == "AAK")?.Value ?? string.Empty;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(wzNumber)) return ediCustomerOrderDto;
|
|
||||||
|
|
||||||
logger.LogError($"Brak numeru WZ dla zamówienia {orderModel.ScheduleOrderDto.PONum}. Pomijam zamówienie.",
|
|
||||||
null);
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override EdiCustomerOrderLineDto? CreateCustomerOrderLine(ScheduleOrderDetailDto scheduleOrderDetail, OrderModel orderModel,
|
protected override EdiCustomerOrderLineDto? CreateCustomerOrderLine(ScheduleOrderDetailDto scheduleOrderDetail, OrderModel orderModel,
|
||||||
@@ -62,8 +51,8 @@ public class ClientBrose(
|
|||||||
{
|
{
|
||||||
DateTime closestSendingDate = GetClosestSendingDate(DateTime.Now);
|
DateTime closestSendingDate = GetClosestSendingDate(DateTime.Now);
|
||||||
|
|
||||||
string wzNumber = orderModel.ScheduleOrderDto.ScheduleOrderDetails.First().ScheduleOrderDetailMiscs
|
string? wzNumber = orderModel.ScheduleOrderDto.ScheduleOrderDetails.FirstOrDefault()?.ScheduleOrderDetailMiscs?
|
||||||
.First(x => x.Type == "AAK").Value;
|
.FirstOrDefault(x => x.Type == "AAK")?.Value;
|
||||||
|
|
||||||
List<MaterialTransactionDto>? transitMaterialTransactionsByCurrentWz =
|
List<MaterialTransactionDto>? transitMaterialTransactionsByCurrentWz =
|
||||||
appService.GetTransitMaterialTransactionsByCurrentWz(wzNumber).Result.FirstOrDefault().Value;
|
appService.GetTransitMaterialTransactionsByCurrentWz(wzNumber).Result.FirstOrDefault().Value;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using DelforSenders.Models;
|
|||||||
using DelforSenders.Services;
|
using DelforSenders.Services;
|
||||||
using FaKrosnoEfDataModel.Dtos;
|
using FaKrosnoEfDataModel.Dtos;
|
||||||
using SytelineSaAppEfDataModel.Dtos;
|
using SytelineSaAppEfDataModel.Dtos;
|
||||||
|
using Action = DelforSenders.Enums.Action;
|
||||||
|
|
||||||
namespace DelforSenders.Core;
|
namespace DelforSenders.Core;
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ public class App(IAppService service, IClientFactory clientFactory, IValidator v
|
|||||||
IList<ItemMapperModel> itemMappers = service.GetItemMappers();
|
IList<ItemMapperModel> itemMappers = service.GetItemMappers();
|
||||||
IList<OrderMapperModel> orderMappers = service.GetOrderMappers();
|
IList<OrderMapperModel> orderMappers = service.GetOrderMappers();
|
||||||
IList<CustomerMapperModel> customerMappers = service.GetCustomerMappers();
|
IList<CustomerMapperModel> customerMappers = service.GetCustomerMappers();
|
||||||
|
IList<ItemQuantityMapper> itemQuantityMappers = service.GetItemQuantityMappers();
|
||||||
IList<EdiCustomerOrderDto> ediCustomerOrders = new List<EdiCustomerOrderDto>();
|
IList<EdiCustomerOrderDto> ediCustomerOrders = new List<EdiCustomerOrderDto>();
|
||||||
|
|
||||||
IEnumerable<ScheduleOrderDto> scheduleOrders = await service.GetScheduleOrders(lastUpdateDate.Value);
|
IEnumerable<ScheduleOrderDto> scheduleOrders = await service.GetScheduleOrders(lastUpdateDate.Value);
|
||||||
@@ -43,6 +45,7 @@ public class App(IAppService service, IClientFactory clientFactory, IValidator v
|
|||||||
{
|
{
|
||||||
logger.LogInfo($"{scheduleOrder.ID} - Rozpoczęcie procesowania zamówienia");
|
logger.LogInfo($"{scheduleOrder.ID} - Rozpoczęcie procesowania zamówienia");
|
||||||
|
|
||||||
|
MapItemQuantityIfNeeded(scheduleOrder, itemQuantityMappers);
|
||||||
MapItemNumberIfNeeded(scheduleOrder, itemMappers);
|
MapItemNumberIfNeeded(scheduleOrder, itemMappers);
|
||||||
MapOrderNumberIfNeeded(scheduleOrder, orderMappers);
|
MapOrderNumberIfNeeded(scheduleOrder, orderMappers);
|
||||||
|
|
||||||
@@ -203,6 +206,66 @@ public class App(IAppService service, IClientFactory clientFactory, IValidator v
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void MapItemQuantityIfNeeded(ScheduleOrderDto scheduleOrder,
|
||||||
|
IList<ItemQuantityMapper> itemQuantityMappers)
|
||||||
|
{
|
||||||
|
if (itemQuantityMappers.All(x => x.OrderNumber != scheduleOrder.PONum)) return;
|
||||||
|
|
||||||
|
foreach (ScheduleOrderDetailDto scheduleOrderDetail in scheduleOrder.ScheduleOrderDetails)
|
||||||
|
{
|
||||||
|
if (itemQuantityMappers.FirstOrDefault(x =>
|
||||||
|
x.Item == scheduleOrderDetail.Sh_productCode && x.OrderNumber == scheduleOrder.PONum) is
|
||||||
|
{ } mapper)
|
||||||
|
{
|
||||||
|
switch (mapper.Action)
|
||||||
|
{
|
||||||
|
case Action.Add:
|
||||||
|
{
|
||||||
|
foreach (ScheduleOrderDetailDetailDto scheduleOrderDetailDetailDto in scheduleOrderDetail
|
||||||
|
.ScheduleOrderDetailDetails)
|
||||||
|
{
|
||||||
|
scheduleOrderDetailDetailDto.Qty += mapper.Modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Action.Distract:
|
||||||
|
{
|
||||||
|
foreach (ScheduleOrderDetailDetailDto scheduleOrderDetailDetailDto in scheduleOrderDetail
|
||||||
|
.ScheduleOrderDetailDetails)
|
||||||
|
{
|
||||||
|
scheduleOrderDetailDetailDto.Qty -= mapper.Modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Action.Multiply:
|
||||||
|
{
|
||||||
|
foreach (ScheduleOrderDetailDetailDto scheduleOrderDetailDetailDto in scheduleOrderDetail
|
||||||
|
.ScheduleOrderDetailDetails)
|
||||||
|
{
|
||||||
|
scheduleOrderDetailDetailDto.Qty *= mapper.Modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Action.Divide:
|
||||||
|
{
|
||||||
|
foreach (ScheduleOrderDetailDetailDto scheduleOrderDetailDetailDto in scheduleOrderDetail
|
||||||
|
.ScheduleOrderDetailDetails)
|
||||||
|
{
|
||||||
|
scheduleOrderDetailDetailDto.Qty /= mapper.Modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void MapItemNumberIfNeeded(ScheduleOrderDto scheduleOrder, IList<ItemMapperModel> itemMappers)
|
private static void MapItemNumberIfNeeded(ScheduleOrderDto scheduleOrder, IList<ItemMapperModel> itemMappers)
|
||||||
{
|
{
|
||||||
foreach (ScheduleOrderDetailDto scheduleOrderDetail in scheduleOrder.ScheduleOrderDetails)
|
foreach (ScheduleOrderDetailDto scheduleOrderDetail in scheduleOrder.ScheduleOrderDetails)
|
||||||
|
|||||||
@@ -25,5 +25,7 @@ public class CustomerOrderLineEnricher: IEnricher<EdiCustomerOrderLineDto, Custo
|
|||||||
{
|
{
|
||||||
entity.QtyPerBox = input.Uf_FKR_EDI_BLN_QtyPerBox;
|
entity.QtyPerBox = input.Uf_FKR_EDI_BLN_QtyPerBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity.Uom = input.UM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ public class ItemEnricher : IEnricher<EdiCustomerOrderLineDto, ItemDto>
|
|||||||
{
|
{
|
||||||
public void Enrich(EdiCustomerOrderLineDto entity, ItemDto input)
|
public void Enrich(EdiCustomerOrderLineDto entity, ItemDto input)
|
||||||
{
|
{
|
||||||
entity.Uom = input.UM;
|
|
||||||
entity.Description = input.Description;
|
entity.Description = input.Description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,15 @@ public class ItemPriceEnricher : IEnricher<EdiCustomerOrderLineDto, ItemCustPric
|
|||||||
{
|
{
|
||||||
public void Enrich(EdiCustomerOrderLineDto entity, ItemCustPriceAllDto input)
|
public void Enrich(EdiCustomerOrderLineDto entity, ItemCustPriceAllDto input)
|
||||||
{
|
{
|
||||||
entity.ContPriceReleased = input.ContPrice;
|
if (entity.Uom == "sst")
|
||||||
entity.ContPrice = input.ContPrice;
|
{
|
||||||
|
entity.ContPriceReleased /= 100;
|
||||||
|
entity.ContPrice *= 100;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity.ContPriceReleased = input.ContPrice;
|
||||||
|
entity.ContPrice = input.ContPrice;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
DelforSender/Enums/Action.cs
Normal file
9
DelforSender/Enums/Action.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DelforSenders.Enums;
|
||||||
|
|
||||||
|
public enum Action
|
||||||
|
{
|
||||||
|
Add,
|
||||||
|
Distract,
|
||||||
|
Multiply,
|
||||||
|
Divide
|
||||||
|
}
|
||||||
5
DelforSender/Models/ItemQuantityMapper.cs
Normal file
5
DelforSender/Models/ItemQuantityMapper.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using Action = DelforSenders.Enums.Action;
|
||||||
|
|
||||||
|
namespace DelforSenders.Models;
|
||||||
|
|
||||||
|
public record ItemQuantityMapper(string OrderNumber,string Item, Action Action, int Modifier);
|
||||||
@@ -158,6 +158,14 @@ public class AppService(IRestClient restClient, IConfiguration configuration, IE
|
|||||||
|
|
||||||
return customerMappers;
|
return customerMappers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IList<ItemQuantityMapper> GetItemQuantityMappers()
|
||||||
|
{
|
||||||
|
var itemQuantityMappers = configuration.GetSection("ItemQuantityMAppers").Get<List<ItemQuantityMapper>>() ??
|
||||||
|
new List<ItemQuantityMapper>();
|
||||||
|
|
||||||
|
return itemQuantityMappers;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task GenerateAndSendEmail()
|
public async Task GenerateAndSendEmail()
|
||||||
{
|
{
|
||||||
@@ -172,11 +180,16 @@ public class AppService(IRestClient restClient, IConfiguration configuration, IE
|
|||||||
await emailGeneratorService.SendEmailAsync(emailModel);
|
await emailGeneratorService.SendEmailAsync(emailModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDictionary<string, List<MaterialTransactionDto>>> GetTransitMaterialTransactionsByCurrentWz(string wzNumber)
|
public async Task<IDictionary<string, List<MaterialTransactionDto>>> GetTransitMaterialTransactionsByCurrentWz(string? wzNumber)
|
||||||
{
|
{
|
||||||
IDictionary<string, List<MaterialTransactionDto>> transitMaterialTransactionsByCurrentWz =
|
IDictionary<string, List<MaterialTransactionDto>> transitMaterialTransactionsByCurrentWz =
|
||||||
new Dictionary<string, List<MaterialTransactionDto>>();
|
new Dictionary<string, List<MaterialTransactionDto>>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(wzNumber))
|
||||||
|
{
|
||||||
|
return transitMaterialTransactionsByCurrentWz;
|
||||||
|
}
|
||||||
|
|
||||||
IEnumerable<MaterialTransactionDto> materialTransactions =
|
IEnumerable<MaterialTransactionDto> materialTransactions =
|
||||||
await GetMaterialTransactions(new HashSet<string> { wzNumber });
|
await GetMaterialTransactions(new HashSet<string> { wzNumber });
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ public interface IAppService
|
|||||||
IList<ItemMapperModel> GetItemMappers();
|
IList<ItemMapperModel> GetItemMappers();
|
||||||
IList<OrderMapperModel> GetOrderMappers();
|
IList<OrderMapperModel> GetOrderMappers();
|
||||||
IList<CustomerMapperModel> GetCustomerMappers();
|
IList<CustomerMapperModel> GetCustomerMappers();
|
||||||
|
IList<ItemQuantityMapper> GetItemQuantityMappers();
|
||||||
Task<bool> SaveOrders(IList<EdiCustomerOrderDto> ediCustomerOrders);
|
Task<bool> SaveOrders(IList<EdiCustomerOrderDto> ediCustomerOrders);
|
||||||
Task<bool> SaveEdiCustomerOrderImport(IList<EdiCustomerOrderImportDto> ediCustomerOrderImports);
|
Task<bool> SaveEdiCustomerOrderImport(IList<EdiCustomerOrderImportDto> ediCustomerOrderImports);
|
||||||
Task<bool> SaveLog(EdiLogDto ediLog);
|
Task<bool> SaveLog(EdiLogDto ediLog);
|
||||||
Task GenerateAndSendEmail();
|
Task GenerateAndSendEmail();
|
||||||
Task<IDictionary<string, List<MaterialTransactionDto>>> GetTransitMaterialTransactionsByCurrentWz(string wzNumber);
|
Task<IDictionary<string, List<MaterialTransactionDto>>> GetTransitMaterialTransactionsByCurrentWz(string? wzNumber);
|
||||||
}
|
}
|
||||||
@@ -12,18 +12,11 @@ public class ValidatorService(IRestClient restClient) : IValidatorService
|
|||||||
request.AddQueryParameter("customerNumber", customer.CustNum);
|
request.AddQueryParameter("customerNumber", customer.CustNum);
|
||||||
request.AddQueryParameter("customerSequence", customer.CustSeq.ToString());
|
request.AddQueryParameter("customerSequence", customer.CustSeq.ToString());
|
||||||
request.AddQueryParameter("poNumber", scheduleOrder.PONum);
|
request.AddQueryParameter("poNumber", scheduleOrder.PONum);
|
||||||
|
|
||||||
var response = await restClient.ExecuteAsync<CustomerOrderDto>(request);
|
var response = await restClient.ExecuteAsync<CustomerOrderDto>(request);
|
||||||
if (response is { IsSuccessful: true, Data: not null }) return true;
|
return response is { IsSuccessful: true, Data: not null };
|
||||||
|
|
||||||
request = new RestRequest("api/customerOrders/list-by-customer");
|
|
||||||
request.AddQueryParameter("customerNumber", customer.CustNum);
|
|
||||||
request.AddQueryParameter("customerSequence", customer.CustSeq.ToString());
|
|
||||||
|
|
||||||
var listResponse = await restClient.ExecuteAsync<IList<CustomerOrderDto>>(request);
|
|
||||||
return listResponse is { IsSuccessful: true, Data.Count: > 0 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ItemCustDto?> GetItemCust(string itemNumber, string customerNumber)
|
public async Task<ItemCustDto?> GetItemCust(string itemNumber, string customerNumber)
|
||||||
{
|
{
|
||||||
var request = new RestRequest("api/ItemCust");
|
var request = new RestRequest("api/ItemCust");
|
||||||
|
|||||||
@@ -36,6 +36,21 @@
|
|||||||
"OrderNumber": "FP1015",
|
"OrderNumber": "FP1015",
|
||||||
"Recipient": 16,
|
"Recipient": 16,
|
||||||
"NewOrderNumber": "940088"
|
"NewOrderNumber": "940088"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OrderNumber": "0092190784",
|
||||||
|
"Recipient": 27,
|
||||||
|
"NewOrderNumber": "92190784"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OrderNumber": "000098459753",
|
||||||
|
"Recipient": 37,
|
||||||
|
"NewOrderNumber": "98459753"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OrderNumber": "0092188648",
|
||||||
|
"Recipient": 27,
|
||||||
|
"NewOrderNumber": "92188648"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"CustomerMappers": [
|
"CustomerMappers": [
|
||||||
@@ -46,6 +61,14 @@
|
|||||||
"CustomerSequenceNumber": 1
|
"CustomerSequenceNumber": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"ItemQuantityMappers": [
|
||||||
|
{
|
||||||
|
"OrderNumber": "000098459753",
|
||||||
|
"Item": "FA33590.97A",
|
||||||
|
"Action": "Divide",
|
||||||
|
"Modifier": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
"Customers": [
|
"Customers": [
|
||||||
{
|
{
|
||||||
"TpCode": "000254206547900919",
|
"TpCode": "000254206547900919",
|
||||||
|
|||||||
22
DevelopmentAgent/DevelopmentAgent.csproj
Normal file
22
DevelopmentAgent/DevelopmentAgent.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>DeploymentAgent</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
|
||||||
|
<PackageReference Include="Services" Version="2.0.0-alpha.0.149" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Prompts\TestProjectGenerator.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
36
DevelopmentAgent/Program.cs
Normal file
36
DevelopmentAgent/Program.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using DeploymentAgent.Shell;
|
||||||
|
using DeploymentAgent.TestsGenerator;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Services.Gitea;
|
||||||
|
using Services.OpenAI;
|
||||||
|
using Services.Vault;
|
||||||
|
|
||||||
|
IHost builder = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureServices((_, services) =>
|
||||||
|
{
|
||||||
|
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||||
|
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||||
|
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||||
|
|
||||||
|
services.AddSingleton<IVaultService>(vaultService);
|
||||||
|
|
||||||
|
services.AddScoped<IGiteaService, GiteaService>();
|
||||||
|
services.AddScoped<IOpenAiService, OpenAiService>();
|
||||||
|
services.AddScoped<IShellRunner, ShellRunner>();
|
||||||
|
services.AddScoped<ITestsGeneratorAgent, TestsGeneratorAgent>();
|
||||||
|
}).Build();
|
||||||
|
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Usage: DeploymentAgent <command> [options]");
|
||||||
|
Console.WriteLine("Commands: tests-generator");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args[0] switch
|
||||||
|
{
|
||||||
|
"tests-generator" => await builder.Services.GetRequiredService<ITestsGeneratorAgent>()
|
||||||
|
.GenerateTestsAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
147
DevelopmentAgent/Prompts/TestProjectGenerator.txt
Normal file
147
DevelopmentAgent/Prompts/TestProjectGenerator.txt
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
Jesteś C# Developerem specjalizującym się w tworzeniu projektów testowych dla aplikacji .NET.
|
||||||
|
|
||||||
|
Wejście:
|
||||||
|
Dostajesz pojedynczy obiekt JSON w formacie:
|
||||||
|
{
|
||||||
|
"projectName": string, // ścieżka katalogu projektu, dla którego mają zostać wygenerowane testy (np. "BroseCumulativeReport" lub "src/BroseCumulativeReport")
|
||||||
|
"solutionPath": string, // pełna ścieżka do pliku .sln, do którego ma zostać dodany projekt testowy (np. "/Users/user/RiderProjects/FA/FA.sln")
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": string, // ścieżka pliku C# względem katalogu solution (np. "BroseCumulativeReport/Services/FooService.cs")
|
||||||
|
"content": string // pełna zawartość pliku C#
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Twoje zadanie:
|
||||||
|
|
||||||
|
1. Przeanalizuj wszystkie pliki z pola "files":
|
||||||
|
- wykryj publiczne klasy, metody i właściwości,
|
||||||
|
- wygeneruj testy jednostkowe pokrywające każdą publiczną metodę i właściwość,
|
||||||
|
- uwzględnij różne scenariusze, w tym przypadki brzegowe i obsługę wyjątków,
|
||||||
|
- użyj xUnit jako frameworka testowego,
|
||||||
|
- tam, gdzie klasy mają zależności (np. przez konstruktor lub wstrzykiwanie interfejsów), sugeruj ich mockowanie za pomocą Moq (np. new Mock<IMyService>()).
|
||||||
|
|
||||||
|
2. Na podstawie wartości "projectName":
|
||||||
|
- przyjmij, że "projectName" wskazuje katalog projektu głównego względem katalogu solution (np. "BroseCumulativeReport" albo "src/BroseCumulativeReport"),
|
||||||
|
- pobierz ostatni segment tej ścieżki jako nazwę projektu głównego, np.:
|
||||||
|
- projectName: "BroseCumulativeReport" → nazwa projektu głównego: "BroseCumulativeReport",
|
||||||
|
- projectName: "src/BroseCumulativeReport" → nazwa projektu głównego: "BroseCumulativeReport",
|
||||||
|
- nazwij projekt testowy "{NazwaProjektuGłównego}.Tests", np. "BroseCumulativeReport.Tests".
|
||||||
|
|
||||||
|
3. Przygotuj plik projektu testowego "{TestProjectName}.csproj" o zawartości:
|
||||||
|
|
||||||
|
- <Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
- <PropertyGroup> z:
|
||||||
|
- <TargetFramework> dopasowany do projektu głównego; jeśli nie znasz dokładnej wartości, użyj "net8.0",
|
||||||
|
- <IsPackable>false</IsPackable>.
|
||||||
|
- <ItemGroup> z referencjami do paczek:
|
||||||
|
- Microsoft.NET.Test.Sdk
|
||||||
|
- xunit
|
||||||
|
- xunit.runner.visualstudio
|
||||||
|
- coverlet.collector
|
||||||
|
- Moq
|
||||||
|
- <ItemGroup> z:
|
||||||
|
- <ProjectReference Include="ŚCIEŻKA_DO_PROJEKTU_GŁÓWNEGO.csproj" />
|
||||||
|
|
||||||
|
WAŻNE (ProjectReference i ścieżki):
|
||||||
|
|
||||||
|
- Przyjmij standardowy układ katalogów:
|
||||||
|
- katalog z solutionPath (ROOT), np. "/Users/user/RiderProjects/FA",
|
||||||
|
- w nim katalog projektu głównego, np. "BroseCumulativeReport" (ostatni segment projectName),
|
||||||
|
- w nim plik .csproj projektu głównego: "{NazwaProjektuGłównego}.csproj" (np. "BroseCumulativeReport/BroseCumulativeReport.csproj"),
|
||||||
|
- projekt testowy w katalogu "{TestProjectName}" obok projektu głównego: "{TestProjectName}" pod ROOT (np. "BroseCumulativeReport.Tests").
|
||||||
|
- Katalog projektu testowego będzie więc:
|
||||||
|
- ROOT/{TestProjectName}, np. "/Users/user/RiderProjects/FA/BroseCumulativeReport.Tests".
|
||||||
|
- Ścieżka w `<ProjectReference Include="...">` musi być RELATYWNA względem katalogu projektu testowego (katalogu, w którym leży plik .csproj testów), czyli:
|
||||||
|
- z "BroseCumulativeReport.Tests" przechodzisz poziom wyżej do ROOT, a następnie do katalogu projektu głównego.
|
||||||
|
- poprawny wzór:
|
||||||
|
"../{NazwaProjektuGłównego}/{NazwaProjektuGłównego}.csproj"
|
||||||
|
- PRZYKŁAD:
|
||||||
|
- solutionPath: "/Users/piotrkus/RiderProjects/FA/FA.sln"
|
||||||
|
- projectName: "BroseCumulativeReport"
|
||||||
|
- nazwa projektu głównego: "BroseCumulativeReport"
|
||||||
|
- katalog projektu testowego: "/Users/piotrkus/RiderProjects/FA/BroseCumulativeReport.Tests"
|
||||||
|
- katalog projektu głównego: "/Users/piotrkus/RiderProjects/FA/BroseCumulativeReport"
|
||||||
|
- wtedy w pliku BroseCumulativeReport.Tests.csproj wstaw:
|
||||||
|
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||||
|
- Wygeneruj `<ProjectReference Include="...">` jako LITERALNY tekst w XML, bez używania zmiennych powłoki bash (nie używaj $MAIN_PROJECT_REL_PATH). Wstaw pełną ścieżkę względną względem katalogu projektu testowego.
|
||||||
|
|
||||||
|
4. Zaprojektuj strukturę katalogów testowych:
|
||||||
|
|
||||||
|
- katalog główny projektu testowego: "{TestProjectName}" (np. "BroseCumulativeReport.Tests"),
|
||||||
|
- wewnątrz niego ewentualne podkatalogi odzwierciedlające strukturę namespace/folderów projektu głównego (np. "Model", "Configuration"),
|
||||||
|
- dla każdej publicznej klasy z projektu głównego wygeneruj odpowiadający plik testowy:
|
||||||
|
- np. BroseCumulativeReport.App → AppTests.cs,
|
||||||
|
- BroseCumulativeReport.ExcelGenerator → ExcelGeneratorTests.cs,
|
||||||
|
- BroseCumulativeReport.Model.MainRow → Model/MainRowTests.cs, itd.
|
||||||
|
- w każdej klasie testowej:
|
||||||
|
- wygeneruj metody testowe dla wszystkich publicznych metod i właściwości,
|
||||||
|
- używaj nazewnictwa w stylu: MethodName_ShouldDoSomething_WhenCondition,
|
||||||
|
- korzystaj z [Fact] dla prostych przypadków oraz [Theory] z [InlineData(...)] tam, gdzie można sensownie zaproponować różne dane wejściowe,
|
||||||
|
- stosuj strukturę Arrange / Act / Assert z TODO-komentarzami tam, gdzie potrzebne są decyzje domenowe,
|
||||||
|
- dla zależności korzystaj z Moq (np. var dependency = new Mock<IMyService>(); i dependency.Object wstrzykiwane do konstruktora).
|
||||||
|
|
||||||
|
5. Na podstawie powyższych informacji wygeneruj skrypt powłoki /bin/sh, który:
|
||||||
|
|
||||||
|
- jest samowystarczalny: NIE czyta JSON-a ani żadnych danych wejściowych,
|
||||||
|
- nie przyjmuje żadnych argumentów,
|
||||||
|
- zakłada, że:
|
||||||
|
- plik solution znajduje się dokładnie pod ścieżką podaną w "solutionPath" w wejściowym JSON-ie,
|
||||||
|
- projekt testowy ma być utworzony w tym samym katalogu co plik solution (czyli obok pliku .sln, nie w katalogu projektu głównego).
|
||||||
|
|
||||||
|
Skrypt powinien wykonywać następujące kroki:
|
||||||
|
|
||||||
|
1) Ustawić zmienną SOLUTION_PATH na wartość z pola "solutionPath" (osadzoną na stałe w skrypcie, np. SOLUTION_PATH="/Users/piotrkus/RiderProjects/FA/FA.sln").
|
||||||
|
2) Wyznaczyć SOLUTION_DIR jako katalog nadrzędny pliku solution:
|
||||||
|
SOLUTION_DIR=$(dirname "$SOLUTION_PATH")
|
||||||
|
3) Utworzyć katalog projektu testowego:
|
||||||
|
TEST_PROJECT_NAME="{TestProjectName}" (np. "BroseCumulativeReport.Tests")
|
||||||
|
TEST_PROJECT_DIR="$SOLUTION_DIR/$TEST_PROJECT_NAME"
|
||||||
|
oraz podkatalogi, które zaprojektowałeś (np. "$TEST_PROJECT_DIR/Model", "$TEST_PROJECT_DIR/Configuration").
|
||||||
|
4) W katalogu TEST_PROJECT_DIR zapisać plik "{TestProjectName}.csproj" z wygenerowaną zawartością, używając heredoca.
|
||||||
|
- W pliku .csproj wstaw literalną ścieżkę ProjectReference zgodnie z regułą:
|
||||||
|
"../{NazwaProjektuGłównego}/{NazwaProjektuGłównego}.csproj"
|
||||||
|
- Możesz użyć heredoca z 'EOF', ponieważ nie potrzebujesz interpolacji zmiennych bash wewnątrz XML.
|
||||||
|
5) W katalogu TEST_PROJECT_DIR utworzyć strukturę podkatalogów testowych i zapisać wszystkie wygenerowane pliki testowe (po jednym heredocu na plik).
|
||||||
|
6) Wykonać polecenie:
|
||||||
|
dotnet sln "$SOLUTION_PATH" add "$TEST_PROJECT_DIR/{TestProjectName}.csproj"
|
||||||
|
aby dodać projekt testowy do rozwiązania.
|
||||||
|
7) Wypisać na stderr lub stdout krótki komunikat o sukcesie (np. "Test project {TestProjectName} created and added to solution").
|
||||||
|
|
||||||
|
Skrypt może zawierać podstawowe sprawdzenia:
|
||||||
|
- czy polecenie "dotnet" jest dostępne,
|
||||||
|
- czy plik solution istnieje pod ścieżką SOLUTION_PATH,
|
||||||
|
- w razie krytycznych błędów powinien zakończyć się niezerowym kodem wyjścia.
|
||||||
|
|
||||||
|
WAŻNE:
|
||||||
|
|
||||||
|
- Cała logika analizy kodu C# i generowania treści plików testowych musi być wykonana przez Ciebie PRZED wygenerowaniem skryptu (na podstawie "files").
|
||||||
|
- Skrypt ma tylko:
|
||||||
|
- utworzyć katalog projektu testowego,
|
||||||
|
- zapisać gotowy plik .csproj i pliki testowe (dokładnie takie, jakie wygenerujesz),
|
||||||
|
- dodać projekt do solution.
|
||||||
|
- Skrypt NIE może parsować JSON-a, NIE może czytać plików źródłowych projektu głównego ani próbować ich analizować – wszystko, czego potrzebuje, musi być już wpisane w jego treść.
|
||||||
|
- W szczególności w pliku .csproj:
|
||||||
|
- NIE używaj zmiennych powłoki takich jak $MAIN_PROJECT_REL_PATH wewnątrz `<ProjectReference Include="...">`.
|
||||||
|
- Wstaw pełną ścieżkę względną względem katalogu projektu testowego, np.:
|
||||||
|
<ProjectReference Include="../BroseCumulativeReport/BroseCumulativeReport.csproj" />
|
||||||
|
|
||||||
|
FORMAT ODPOWIEDZI:
|
||||||
|
|
||||||
|
Zwróć dokładnie jeden obiekt JSON o strukturze:
|
||||||
|
|
||||||
|
{
|
||||||
|
"script": "KOMPLETNY SKRYPT /bin/sh",
|
||||||
|
"comments": "Dodatkowe uwagi lub instrukcje dotyczące użycia skryptu"
|
||||||
|
}
|
||||||
|
|
||||||
|
Zasady formatowania:
|
||||||
|
- W polu "script" umieść kompletny skrypt /bin/sh gotowy do zapisania do pliku i uruchomienia bez modyfikacji.
|
||||||
|
- Skrypt w "script" nie może zawierać żadnych znaczników Markdown ani bloków ``` – ma to być czysty tekst skryptu.
|
||||||
|
- W polu "comments" podaj zwięzłe informacje:
|
||||||
|
- jak uruchomić skrypt,
|
||||||
|
- jakie są wymagania (np. zainstalowany .NET SDK),
|
||||||
|
- ewentualne założenia dotyczące ścieżek.
|
||||||
|
- Nie dodawaj żadnego tekstu poza tym jednym obiektem JSON.
|
||||||
|
- Upewnij się, że wygenerowany JSON jest poprawny składniowo (prawidłowe cudzysłowy, przecinki, brak zbędnych znaków).
|
||||||
6
DevelopmentAgent/Shell/IShellRunner.cs
Normal file
6
DevelopmentAgent/Shell/IShellRunner.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DeploymentAgent.Shell;
|
||||||
|
|
||||||
|
public interface IShellRunner
|
||||||
|
{
|
||||||
|
(int ExitCode, string StdOut, string StdErr) RunScript(string script);
|
||||||
|
}
|
||||||
35
DevelopmentAgent/Shell/ShellRunner.cs
Normal file
35
DevelopmentAgent/Shell/ShellRunner.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace DeploymentAgent.Shell;
|
||||||
|
|
||||||
|
public class ShellRunner : IShellRunner
|
||||||
|
{
|
||||||
|
public (int ExitCode, string StdOut, string StdErr) RunScript(string script)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "/bin/sh",
|
||||||
|
Arguments = "-s",
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = psi;
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
process.StandardInput.WriteLine(script);
|
||||||
|
process.StandardInput.Close();
|
||||||
|
|
||||||
|
var output = process.StandardOutput.ReadToEnd();
|
||||||
|
var error = process.StandardError.ReadToEnd();
|
||||||
|
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
return (process.ExitCode, output, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
DevelopmentAgent/TestsGenerator/ITestsGeneratorAgent.cs
Normal file
6
DevelopmentAgent/TestsGenerator/ITestsGeneratorAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DeploymentAgent.TestsGenerator;
|
||||||
|
|
||||||
|
public interface ITestsGeneratorAgent
|
||||||
|
{
|
||||||
|
Task<int> GenerateTestsAsync(string codePath);
|
||||||
|
}
|
||||||
7
DevelopmentAgent/TestsGenerator/Models/FileModel.cs
Normal file
7
DevelopmentAgent/TestsGenerator/Models/FileModel.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DeploymentAgent.TestsGenerator.Models;
|
||||||
|
|
||||||
|
public class FileModel
|
||||||
|
{
|
||||||
|
public string Path { get; set; } = null!;
|
||||||
|
public string Content { get; set; } = null!;
|
||||||
|
}
|
||||||
8
DevelopmentAgent/TestsGenerator/Models/RequestModel.cs
Normal file
8
DevelopmentAgent/TestsGenerator/Models/RequestModel.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace DeploymentAgent.TestsGenerator.Models;
|
||||||
|
|
||||||
|
public class RequestModel
|
||||||
|
{
|
||||||
|
public string ProjectName { get; set; } = null!;
|
||||||
|
public string SolutionPath { get; set; } = null!;
|
||||||
|
public List<FileModel> Files { get; set; } = new();
|
||||||
|
}
|
||||||
80
DevelopmentAgent/TestsGenerator/TestsGeneratorAgent.cs
Normal file
80
DevelopmentAgent/TestsGenerator/TestsGeneratorAgent.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
using DeploymentAgent.Shell;
|
||||||
|
using DeploymentAgent.TestsGenerator.Models;
|
||||||
|
using Services.OpenAI;
|
||||||
|
|
||||||
|
namespace DeploymentAgent.TestsGenerator;
|
||||||
|
|
||||||
|
public class TestsGeneratorAgent(IOpenAiService openAiService, IShellRunner shellRunner) : ITestsGeneratorAgent
|
||||||
|
{
|
||||||
|
public async Task<int> GenerateTestsAsync(string codePath)
|
||||||
|
{
|
||||||
|
var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
|
||||||
|
"TestProjectGenerator.txt"));
|
||||||
|
|
||||||
|
var requestModel = GetFormattedJsonRequest(codePath);
|
||||||
|
var decisionDoc = await openAiService.GetResponseFromChat(requestModel, prompt);
|
||||||
|
|
||||||
|
if (decisionDoc == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("AI‑Gate: no response from LLM");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var generatedTests = decisionDoc.RootElement.GetProperty("script").GetString();
|
||||||
|
var comments = decisionDoc.RootElement.GetProperty("comments").GetString();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(generatedTests)) return 1;
|
||||||
|
|
||||||
|
var (exitCode, stdout, stderr) = shellRunner.RunScript(generatedTests);
|
||||||
|
Console.WriteLine("AI‑Gate: PowerShell script executed.");
|
||||||
|
Console.WriteLine("Comments from LLM:");
|
||||||
|
Console.WriteLine(comments);
|
||||||
|
Console.WriteLine("Shell output:");
|
||||||
|
Console.WriteLine(exitCode);
|
||||||
|
Console.WriteLine(stdout);
|
||||||
|
Console.WriteLine(stderr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequestModel GetFormattedJsonRequest(string rootPath)
|
||||||
|
{
|
||||||
|
var files = new List<FileModel>();
|
||||||
|
|
||||||
|
foreach (var filePath in Directory.EnumerateFiles(rootPath, "*.cs", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(filePath);
|
||||||
|
files.Add(new FileModel
|
||||||
|
{
|
||||||
|
Path = filePath,
|
||||||
|
Content = content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RequestModel
|
||||||
|
{
|
||||||
|
ProjectName = rootPath,
|
||||||
|
SolutionPath = GetSolutionName(rootPath) ?? string.Empty,
|
||||||
|
Files = files
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetSolutionName(string csprojPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(csprojPath))
|
||||||
|
throw new ArgumentException("projectDirectory is null or empty", nameof(csprojPath));
|
||||||
|
|
||||||
|
var dir = new DirectoryInfo(csprojPath);
|
||||||
|
|
||||||
|
while (dir != null)
|
||||||
|
{
|
||||||
|
var sln = dir.GetFiles("*.sln", SearchOption.TopDirectoryOnly)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (sln != null)
|
||||||
|
return sln.FullName;
|
||||||
|
|
||||||
|
dir = dir.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
FA.sln
103
FA.sln
@@ -19,40 +19,142 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeartBeatClient.Tests", "He
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineAgent", "PipelineAgent\PipelineAgent.csproj", "{348BC96F-2001-4251-BC41-FAFC44B4559F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipelineAgent", "PipelineAgent\PipelineAgent.csproj", "{348BC96F-2001-4251-BC41-FAFC44B4559F}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevelopmentAgent", "DevelopmentAgent\DevelopmentAgent.csproj", "{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BroseCumulativeReport.Tests", "BroseCumulativeReport.Tests\BroseCumulativeReport.Tests.csproj", "{6D4722BD-C891-4C87-9A32-289A2DB92F44}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeartBeatServer.Tests", "HeartBeatServer.Tests\HeartBeatServer.Tests.csproj", "{45544483-3E12-4885-A36A-38455818E296}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4C087BD4-1982-4969-8F11-94DC95579925}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|Any CPU.Build.0 = Release|Any CPU
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{EEC5EEC7-7205-467D-91A7-C8163D2FBB16}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|Any CPU.Build.0 = Release|Any CPU
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{78B009BD-C2D6-4708-B052-58ADEC9FB601}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|Any CPU.Build.0 = Release|Any CPU
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{348BC96F-2001-4251-BC41-FAFC44B4559F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{0C573E4B-BF4B-40BD-B777-38786B8BFDD8}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{6D4722BD-C891-4C87-9A32-289A2DB92F44}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -61,6 +163,7 @@ Global
|
|||||||
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
{F730E58B-A1A7-4F63-93F3-F5AFAC4A26B2} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||||
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
{B0BC96B1-F19A-4C6D-86F4-EA8F90DAC484} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||||
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
{ECF8B455-7DBE-42FD-8A2C-79FDD8C74A62} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||||
|
{45544483-3E12-4885-A36A-38455818E296} = {F351CF33-7A7A-495B-A1C0-4C5888C45CB3}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {5510D1A6-B22F-4C3D-9EA8-66F1676CF7D5}
|
SolutionGuid = {5510D1A6-B22F-4C3D-9EA8-66F1676CF7D5}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
using HeartBeatClients.Core;
|
|
||||||
using FaKrosnoEfDataModel;
|
|
||||||
using HeartBeatClients.Services;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Services.Vault;
|
|
||||||
using SytelineSaAppEfDataModel;
|
|
||||||
using FaKrosnoMappingProfile = FaKrosnoEfDataModel.MappingProfile;
|
|
||||||
using SytelineSaAppMappingProfile = SytelineSaAppEfDataModel.MappingProfile;
|
|
||||||
|
|
||||||
namespace HeartBeatClients;
|
|
||||||
|
|
||||||
public class Builder
|
|
||||||
{
|
|
||||||
public IHostBuilder CreateHostBuilder(string[] args) =>
|
|
||||||
Host.CreateDefaultBuilder(args)
|
|
||||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
|
||||||
{
|
|
||||||
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
|
||||||
})
|
|
||||||
.ConfigureServices((hostContext, services) =>
|
|
||||||
{
|
|
||||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
|
||||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
|
||||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
|
||||||
|
|
||||||
var sytelineConnection = vaultService.GetSecretAsync("database/syteline_prod", "connection_string").GetAwaiter().GetResult();
|
|
||||||
var faKrosnoConnection = vaultService.GetSecretAsync("database/fakrosno", "connection_string").GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
services.AddDbContext<SytelineSaAppDbContext>(options => options.UseSqlServer(sytelineConnection));
|
|
||||||
services.AddDbContext<FaKrosnoDbContext>(options => options.UseSqlServer(faKrosnoConnection));
|
|
||||||
|
|
||||||
services.AddAutoMapper(typeof(FaKrosnoMappingProfile), typeof(SytelineSaAppMappingProfile));
|
|
||||||
services.AddSingleton<IVaultService>(vaultService);
|
|
||||||
services.AddScoped<IEmailGeneratorService, EmailGeneratorService>();
|
|
||||||
services.AddScoped<IHeartBeatDecisionService, HeartBeatDecisionService>();
|
|
||||||
services.AddScoped<IApp, App>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -6,44 +6,58 @@ using Services.Vault;
|
|||||||
|
|
||||||
namespace HeartBeatClients.Core;
|
namespace HeartBeatClients.Core;
|
||||||
|
|
||||||
public class App(IConfiguration configuration, IEmailGeneratorService emailGeneratorService, IVaultService vaultService) : IApp
|
public class App : IApp
|
||||||
{
|
{
|
||||||
|
private readonly IConfiguration configuration;
|
||||||
|
private readonly IEmailGeneratorService emailGeneratorService;
|
||||||
|
private readonly IVaultService vaultService;
|
||||||
|
|
||||||
|
public App(IConfiguration configuration, IEmailGeneratorService emailGeneratorService, IVaultService vaultService)
|
||||||
|
{
|
||||||
|
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
this.emailGeneratorService = emailGeneratorService ?? throw new ArgumentNullException(nameof(emailGeneratorService));
|
||||||
|
this.vaultService = vaultService ?? throw new ArgumentNullException(nameof(vaultService));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RunAsync()
|
public async Task RunAsync()
|
||||||
{
|
{
|
||||||
var contaboConnection = await vaultService.GetSecretAsync("database/contabo", "connection_string", "secret");
|
var contaboConnection = await vaultService.GetSecretAsync("database/contabo", "connection_string", "secret");
|
||||||
if (string.IsNullOrWhiteSpace(contaboConnection))
|
if (string.IsNullOrWhiteSpace(contaboConnection))
|
||||||
{
|
{
|
||||||
// Consider logging error or throwing as connection string must be valid
|
|
||||||
throw new InvalidOperationException("Database connection string cannot be null or empty.");
|
throw new InvalidOperationException("Database connection string cannot be null or empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var heartbeatSection = configuration.GetSection("HeartBeatClient");
|
var heartbeatSection = configuration.GetSection("HeartBeatClient");
|
||||||
if (!int.TryParse(heartbeatSection["HeartBeatInterval"], out int heartBeatInterval))
|
if (!int.TryParse(heartbeatSection["HeartBeatInterval"], out int heartBeatInterval))
|
||||||
{
|
{
|
||||||
// You might want to provide a default or throw
|
heartBeatInterval = 5; // Default value
|
||||||
heartBeatInterval = 0;
|
}
|
||||||
|
|
||||||
|
if (heartBeatInterval <= 0)
|
||||||
|
{
|
||||||
|
// Consider logging a warning that heartbeat interval is invalid
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultModel? result = await GetLastUpdateDate(contaboConnection);
|
ResultModel? result = await GetLastUpdateDate(contaboConnection);
|
||||||
|
|
||||||
DateTime now = DateTime.Now;
|
|
||||||
DateTime lastUpdateDate = result?.LastUpdateDate.ToUniversalTime() ?? DateTime.MinValue;
|
DateTime lastUpdateDate = result?.LastUpdateDate.ToUniversalTime() ?? DateTime.MinValue;
|
||||||
|
|
||||||
double minutesDifference = (now - lastUpdateDate).TotalMinutes;
|
double minutesDifference = (DateTime.UtcNow - lastUpdateDate).TotalMinutes;
|
||||||
|
|
||||||
if (minutesDifference > heartBeatInterval && result?.EmailSent == false)
|
if (result != null && minutesDifference > heartBeatInterval && result.EmailSent == false)
|
||||||
{
|
{
|
||||||
await GenerateAlert(lastUpdateDate);
|
await GenerateAlert(lastUpdateDate);
|
||||||
await UpdateHeartBeat(contaboConnection, result!.Id);
|
await UpdateHeartBeat(contaboConnection, result.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GenerateAlert(DateTime lastUpdateDate)
|
private async Task GenerateAlert(DateTime lastUpdateDate)
|
||||||
{
|
{
|
||||||
EmailModel emailModel = new EmailModel
|
var emailModel = new EmailModel
|
||||||
{
|
{
|
||||||
Subject = $"HeartBeat: Serwer nie odpowiada",
|
Subject = $"HeartBeat: Serwer nie odpowiada",
|
||||||
Body = $"Serwer nie odpowiada od {lastUpdateDate}. Proszę o sprawdzenie stanu serwera."
|
Body = $"Serwer nie odpowiada od {lastUpdateDate:u}. Proszę o sprawdzenie stanu serwera."
|
||||||
};
|
};
|
||||||
|
|
||||||
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
||||||
@@ -51,30 +65,32 @@ public class App(IConfiguration configuration, IEmailGeneratorService emailGener
|
|||||||
await emailGeneratorService.SendEmailAsync(emailModel);
|
await emailGeneratorService.SendEmailAsync(emailModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string QueryGetLastUpdate = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\"." +
|
||||||
|
"\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
||||||
|
|
||||||
private async Task<ResultModel?> GetLastUpdateDate(string connectionString)
|
private async Task<ResultModel?> GetLastUpdateDate(string connectionString)
|
||||||
{
|
{
|
||||||
const string query = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\"." +
|
|
||||||
"\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
|
||||||
|
|
||||||
await using var connection = new NpgsqlConnection(connectionString);
|
await using var connection = new NpgsqlConnection(connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
await using var command = new NpgsqlCommand(query, connection);
|
await using var command = new NpgsqlCommand(QueryGetLastUpdate, connection);
|
||||||
await using var reader = await command.ExecuteReaderAsync();
|
await using var reader = await command.ExecuteReaderAsync();
|
||||||
|
|
||||||
if (await reader.ReadAsync())
|
if (await reader.ReadAsync())
|
||||||
{
|
{
|
||||||
return new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2));
|
return new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly string QueryUpdateHeartBeat = "UPDATE \"OrdersManagement\"." +
|
||||||
|
"\"HeartBeat\" SET \"EmailSent\" = true WHERE \"ID\" = @Id;";
|
||||||
|
|
||||||
private async Task UpdateHeartBeat(string connectionString, int id)
|
private async Task UpdateHeartBeat(string connectionString, int id)
|
||||||
{
|
{
|
||||||
const string query = "UPDATE \"OrdersManagement\".\"HeartBeat\" SET \"EmailSent\" = true WHERE \"ID\" = @Id;";
|
|
||||||
|
|
||||||
await using var connection = new NpgsqlConnection(connectionString);
|
await using var connection = new NpgsqlConnection(connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
await using var command = new NpgsqlCommand(query, connection);
|
await using var command = new NpgsqlCommand(QueryUpdateHeartBeat, connection);
|
||||||
command.Parameters.AddWithValue("@Id", id);
|
command.Parameters.AddWithValue("@Id", id);
|
||||||
await command.ExecuteNonQueryAsync();
|
await command.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,49 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using HeartBeatClients;
|
using FaKrosnoEfDataModel;
|
||||||
using HeartBeatClients.Core;
|
using HeartBeatClients.Core;
|
||||||
using HeartBeatClients.Models;
|
using HeartBeatClients.Models;
|
||||||
using HeartBeatClients.Services;
|
using HeartBeatClients.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Services.Vault;
|
using Services.Vault;
|
||||||
|
using SytelineSaAppEfDataModel;
|
||||||
|
using MappingProfile = FaKrosnoEfDataModel.MappingProfile;
|
||||||
|
|
||||||
IHost? host = null;
|
IHost? host = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Builder builder = new Builder();
|
var builder = Host.CreateDefaultBuilder(args)
|
||||||
host = builder.CreateHostBuilder(args).Build();
|
.ConfigureAppConfiguration((_, config) =>
|
||||||
|
{
|
||||||
|
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||||
|
})
|
||||||
|
.ConfigureServices((_, services) =>
|
||||||
|
{
|
||||||
|
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||||
|
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||||
|
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||||
|
|
||||||
|
var sytelineConnection = vaultService.GetSecretAsync("database/syteline_prod", "connection_string").GetAwaiter().GetResult();
|
||||||
|
var faKrosnoConnection = vaultService.GetSecretAsync("database/fakrosno", "connection_string").GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
services.AddDbContext<SytelineSaAppDbContext>(options => options.UseSqlServer(sytelineConnection));
|
||||||
|
services.AddDbContext<FaKrosnoDbContext>(options => options.UseSqlServer(faKrosnoConnection));
|
||||||
|
|
||||||
|
services.AddAutoMapper(typeof(MappingProfile), typeof(SytelineSaAppEfDataModel.MappingProfile));
|
||||||
|
services.AddSingleton<IVaultService>(vaultService);
|
||||||
|
services.AddScoped<IEmailGeneratorService, EmailGeneratorService>();
|
||||||
|
services.AddScoped<IHeartBeatDecisionService, HeartBeatDecisionService>();
|
||||||
|
services.AddScoped<IApp, App>();
|
||||||
|
});
|
||||||
|
|
||||||
|
host = builder.Build();
|
||||||
|
|
||||||
var vaultService = host.Services.GetRequiredService<IVaultService>();
|
var vaultService = host.Services.GetRequiredService<IVaultService>();
|
||||||
var syncfusionLicense = vaultService.GetSecretAsync("licenses/syncfusion", "license_key", "secret")
|
var syncfusionLicense = await vaultService.GetSecretAsync("licenses/syncfusion", "license_key", "secret");
|
||||||
.GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(syncfusionLicense ??
|
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(syncfusionLicense ??
|
||||||
Environment.GetEnvironmentVariable(
|
Environment.GetEnvironmentVariable(
|
||||||
|
|||||||
16
HeartBeatServer.Tests/HeartBeatServer.Tests.csproj
Normal file
16
HeartBeatServer.Tests/HeartBeatServer.Tests.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||||
|
<PackageReference Include="xunit" Version="2.5.0" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.2.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../HeartBeatServer/HeartBeatServer.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
43
HeartBeatServer.Tests/Models/EmailModelTests.cs
Normal file
43
HeartBeatServer.Tests/Models/EmailModelTests.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using HeartBeatServer.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace HeartBeatServer.Tests.Models
|
||||||
|
{
|
||||||
|
public class EmailModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Properties_ShouldInitializeEmptyCollections_AndSetValues()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var emailModel = new EmailModel();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(emailModel.To);
|
||||||
|
Assert.NotNull(emailModel.Cc);
|
||||||
|
Assert.NotNull(emailModel.Bcc);
|
||||||
|
Assert.NotNull(emailModel.Attachments);
|
||||||
|
Assert.Empty(emailModel.To);
|
||||||
|
Assert.Empty(emailModel.Cc);
|
||||||
|
Assert.Empty(emailModel.Bcc);
|
||||||
|
Assert.Empty(emailModel.Attachments);
|
||||||
|
Assert.Equal(string.Empty, emailModel.Subject);
|
||||||
|
Assert.Equal(string.Empty, emailModel.Body);
|
||||||
|
|
||||||
|
// Act - set properties
|
||||||
|
emailModel.Subject = "subject";
|
||||||
|
emailModel.Body = "body";
|
||||||
|
emailModel.To.Add("to@example.com");
|
||||||
|
emailModel.Cc.Add("cc@example.com");
|
||||||
|
emailModel.Bcc.Add("bcc@example.com");
|
||||||
|
emailModel.Attachments.Add("file.txt");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("subject", emailModel.Subject);
|
||||||
|
Assert.Equal("body", emailModel.Body);
|
||||||
|
Assert.Contains("to@example.com", emailModel.To);
|
||||||
|
Assert.Contains("cc@example.com", emailModel.Cc);
|
||||||
|
Assert.Contains("bcc@example.com", emailModel.Bcc);
|
||||||
|
Assert.Contains("file.txt", emailModel.Attachments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
HeartBeatServer.Tests/Models/ResultModelTests.cs
Normal file
39
HeartBeatServer.Tests/Models/ResultModelTests.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using HeartBeatServer.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace HeartBeatServer.Tests.Models
|
||||||
|
{
|
||||||
|
public class ResultModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Properties_ShouldGetSetCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var id = 1;
|
||||||
|
var lastUpdateDate = DateTime.Now;
|
||||||
|
var emailSent = true;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = new ResultModel(id, lastUpdateDate, emailSent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(id, result.Id);
|
||||||
|
Assert.Equal(lastUpdateDate, result.LastUpdateDate);
|
||||||
|
Assert.Equal(emailSent, result.EmailSent);
|
||||||
|
|
||||||
|
// Act - test set accessors
|
||||||
|
var newId = 2;
|
||||||
|
var newDate = DateTime.Now.AddDays(1);
|
||||||
|
var newEmailSent = false;
|
||||||
|
result.Id = newId;
|
||||||
|
result.LastUpdateDate = newDate;
|
||||||
|
result.EmailSent = newEmailSent;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(newId, result.Id);
|
||||||
|
Assert.Equal(newDate, result.LastUpdateDate);
|
||||||
|
Assert.Equal(newEmailSent, result.EmailSent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
HeartBeatServer.Tests/Services/EmailGeneratorServiceTests.cs
Normal file
25
HeartBeatServer.Tests/Services/EmailGeneratorServiceTests.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using HeartBeatServer.Models;
|
||||||
|
using HeartBeatServer.Services;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace HeartBeatServer.Tests.Services
|
||||||
|
{
|
||||||
|
public class EmailGeneratorServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SendEmail_ShouldThrow_WhenMissingSmtpSettings()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var configuration = new ConfigurationBuilder().Build();
|
||||||
|
var emailService = new EmailGeneratorService(configuration);
|
||||||
|
|
||||||
|
var email = new EmailModel();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentException>(() => emailService.SendEmailAsync(email).ConfigureAwait(false).GetAwaiter().GetResult());
|
||||||
|
// Expected to fail int.Parse on Port or missing required data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using HeartBeatServer.Models;
|
||||||
|
using HeartBeatServer.Services;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace HeartBeatServer.Tests.Services
|
||||||
|
{
|
||||||
|
public class IEmailGeneratorServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SendEmail_InterfaceMethod_CallMock()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockService = new Mock<IEmailGeneratorService>();
|
||||||
|
var email = new EmailModel();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
mockService.Object.SendEmailAsync(email).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockService.Verify(s => s.SendEmailAsync(email), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using HeartBeatServers.Core;
|
|
||||||
using FaKrosnoEfDataModel;
|
|
||||||
using HeartBeatServers.Services;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Services.Vault;
|
|
||||||
using SytelineSaAppEfDataModel;
|
|
||||||
using FaKrosnoMappingProfile = FaKrosnoEfDataModel.MappingProfile;
|
|
||||||
using SytelineSaAppMappingProfile = SytelineSaAppEfDataModel.MappingProfile;
|
|
||||||
|
|
||||||
namespace HeartBeatServers;
|
|
||||||
|
|
||||||
public class Builder
|
|
||||||
{
|
|
||||||
public IHostBuilder CreateHostBuilder(string[] args) =>
|
|
||||||
Host.CreateDefaultBuilder(args)
|
|
||||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
|
||||||
{
|
|
||||||
config.AddJsonFile("appSettings.json", optional: false, reloadOnChange: true);
|
|
||||||
})
|
|
||||||
.ConfigureServices((hostContext, services) =>
|
|
||||||
{
|
|
||||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
|
||||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
|
||||||
var vaultService = new VaultService(vaultUrl, vaultToken);
|
|
||||||
|
|
||||||
var sytelineConnection = vaultService.GetSecretAsync("database/syteline_prod", "connection_string").GetAwaiter().GetResult();
|
|
||||||
var faKrosnoConnection = vaultService.GetSecretAsync("database/fakrosno", "connection_string").GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
services.AddDbContext<SytelineSaAppDbContext>(options => options.UseSqlServer(sytelineConnection));
|
|
||||||
services.AddDbContext<FaKrosnoDbContext>(options => options.UseSqlServer(faKrosnoConnection));
|
|
||||||
|
|
||||||
services.AddAutoMapper(typeof(FaKrosnoMappingProfile), typeof(SytelineSaAppMappingProfile));
|
|
||||||
services.AddSingleton<IVaultService>(vaultService);
|
|
||||||
services.AddScoped<IEmailGeneratorService, EmailGeneratorService>();
|
|
||||||
services.AddScoped<IApp, App>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
using HeartBeatServers.Models;
|
using HeartBeatServer.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using Services.Vault;
|
using Services.Vault;
|
||||||
|
|
||||||
namespace HeartBeatServers.Core;
|
namespace HeartBeatServer.Core;
|
||||||
|
|
||||||
public class App(IConfiguration configuration, IVaultService vaultService) : IApp
|
public class App(IConfiguration configuration, IVaultService vaultService) : IApp
|
||||||
{
|
{
|
||||||
|
private const string QueryGetLastUpdate = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\".\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
||||||
|
private const string QueryInsertHeartBeat = "INSERT INTO \"OrdersManagement\".\"HeartBeat\" (\"LastUpdateDate\", \"EmailSent\") VALUES (@LastUpdateDate, @EmailSent);";
|
||||||
|
private const string QueryUpdateHeartBeat = "UPDATE \"OrdersManagement\".\"HeartBeat\" SET \"LastUpdateDate\" = @LastUpdateDate WHERE \"ID\" = @Id;";
|
||||||
|
|
||||||
public async Task RunAsync()
|
public async Task RunAsync()
|
||||||
{
|
{
|
||||||
var contaboConnection = await vaultService.GetSecretAsync("database/contabo", "connection_string", "secret") ?? string.Empty;
|
var contaboConnection = await vaultService.GetSecretAsync("database/contabo", "connection_string", "secret") ?? string.Empty;
|
||||||
|
|
||||||
ResultModel? result = await GetLastUpdateDate(contaboConnection);
|
if (string.IsNullOrWhiteSpace(contaboConnection))
|
||||||
|
throw new InvalidOperationException("Database connection string cannot be null or empty.");
|
||||||
|
|
||||||
|
var result = await GetLastUpdateDate(contaboConnection);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
@@ -25,29 +32,26 @@ public class App(IConfiguration configuration, IVaultService vaultService) : IAp
|
|||||||
|
|
||||||
private async Task<ResultModel?> GetLastUpdateDate(string connectionString)
|
private async Task<ResultModel?> GetLastUpdateDate(string connectionString)
|
||||||
{
|
{
|
||||||
var results = new List<ResultModel?>();
|
|
||||||
string query = "SELECT \"ID\", \"LastUpdateDate\", \"EmailSent\" FROM \"OrdersManagement\".\"HeartBeat\" ORDER BY \"LastUpdateDate\" DESC LIMIT 1;";
|
|
||||||
|
|
||||||
await using var connection = new NpgsqlConnection(connectionString);
|
await using var connection = new NpgsqlConnection(connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
await using var command = new NpgsqlCommand(query, connection);
|
await using var command = new NpgsqlCommand(QueryGetLastUpdate, connection);
|
||||||
await using var reader = await command.ExecuteReaderAsync();
|
await using var reader = await command.ExecuteReaderAsync();
|
||||||
while (reader.Read())
|
|
||||||
|
if (await reader.ReadAsync())
|
||||||
{
|
{
|
||||||
results.Add(new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2)));
|
return new ResultModel(reader.GetInt32(0), reader.GetDateTime(1), reader.GetBoolean(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.FirstOrDefault();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InsertHeartBeat(string connectionString)
|
private async Task InsertHeartBeat(string connectionString)
|
||||||
{
|
{
|
||||||
string query = "INSERT INTO \"OrdersManagement\".\"HeartBeat\" (\"LastUpdateDate\", \"EmailSent\") VALUES (@LastUpdateDate, @EmailSent)";
|
DateTime lastUpdateDate = DateTime.UtcNow;
|
||||||
var lastUpdateDate = DateTime.Now;
|
|
||||||
|
|
||||||
await using var connection = new NpgsqlConnection(connectionString);
|
await using var connection = new NpgsqlConnection(connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
await using var command = new NpgsqlCommand(query, connection);
|
await using var command = new NpgsqlCommand(QueryInsertHeartBeat, connection);
|
||||||
command.Parameters.AddWithValue("@LastUpdateDate", lastUpdateDate);
|
command.Parameters.AddWithValue("@LastUpdateDate", lastUpdateDate);
|
||||||
command.Parameters.AddWithValue("@EmailSent", false);
|
command.Parameters.AddWithValue("@EmailSent", false);
|
||||||
await command.ExecuteNonQueryAsync();
|
await command.ExecuteNonQueryAsync();
|
||||||
@@ -55,12 +59,11 @@ public class App(IConfiguration configuration, IVaultService vaultService) : IAp
|
|||||||
|
|
||||||
private async Task UpdateHeartBeat(string connectionString, int id)
|
private async Task UpdateHeartBeat(string connectionString, int id)
|
||||||
{
|
{
|
||||||
string query = "UPDATE \"OrdersManagement\".\"HeartBeat\" SET \"LastUpdateDate\" = @LastUpdateDate, \"EmailSent\" = false WHERE \"ID\" = @Id";
|
DateTime lastUpdateDate = DateTime.UtcNow;
|
||||||
var lastUpdateDate = DateTime.Now;
|
|
||||||
|
|
||||||
await using var connection = new NpgsqlConnection(connectionString);
|
await using var connection = new NpgsqlConnection(connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
await using var command = new NpgsqlCommand(query, connection);
|
await using var command = new NpgsqlCommand(QueryUpdateHeartBeat, connection);
|
||||||
command.Parameters.AddWithValue("@Id", id);
|
command.Parameters.AddWithValue("@Id", id);
|
||||||
command.Parameters.AddWithValue("@LastUpdateDate", lastUpdateDate);
|
command.Parameters.AddWithValue("@LastUpdateDate", lastUpdateDate);
|
||||||
await command.ExecuteNonQueryAsync();
|
await command.ExecuteNonQueryAsync();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace HeartBeatServers.Core;
|
namespace HeartBeatServer.Core;
|
||||||
|
|
||||||
public interface IApp
|
public interface IApp
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ProjectType>ServerProcess</ProjectType>
|
<ProjectType>ServerProcess</ProjectType>
|
||||||
<RootNamespace>HeartBeatServers</RootNamespace>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace HeartBeatServers.Models;
|
namespace HeartBeatServer.Models;
|
||||||
|
|
||||||
public class EmailModel
|
public class EmailModel
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace HeartBeatServers.Models;
|
namespace HeartBeatServer.Models;
|
||||||
|
|
||||||
public class ResultModel(int id, DateTime lastUpdateDate, bool emailSent)
|
public class ResultModel(int id, DateTime lastUpdateDate, bool emailSent)
|
||||||
{
|
{
|
||||||
public int Id { get; set; } = id;
|
public int Id { get; set; } = id;
|
||||||
public DateTime LastUpdateDate { get; set; } = lastUpdateDate;
|
public DateTime LastUpdateDate { get; set; } = lastUpdateDate;
|
||||||
public bool EmailSent { get; set; } = false;
|
public bool EmailSent { get; set; } = emailSent;
|
||||||
}
|
}
|
||||||
@@ -1,43 +1,92 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using HeartBeatServers;
|
using FaKrosnoEfDataModel;
|
||||||
using HeartBeatServers.Core;
|
using HeartBeatServer.Core;
|
||||||
using HeartBeatServers.Models;
|
using HeartBeatServer.Models;
|
||||||
using HeartBeatServers.Services;
|
using HeartBeatServer.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Services.Vault;
|
||||||
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(
|
using SytelineSaAppEfDataModel;
|
||||||
"NRAiBiAaIQQuGjN/V09+XU9HdVRDX3xKf0x/TGpQb19xflBPallYVBYiSV9jS3tTckVgWHldc3ZUR2lfVE90Vg==");
|
|
||||||
|
|
||||||
IHost? host = null;
|
IHost? host = null;
|
||||||
|
|
||||||
try
|
// Changed Main method to async Task Main to support async operations correctly
|
||||||
|
async Task MainAsync(string[] args)
|
||||||
{
|
{
|
||||||
Builder builder = new Builder();
|
try
|
||||||
host = builder.CreateHostBuilder(args).Build();
|
|
||||||
|
|
||||||
var app = host.Services.GetRequiredService<IApp>();
|
|
||||||
await app.RunAsync();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
|
|
||||||
if (host == null)
|
|
||||||
{
|
{
|
||||||
|
var builder = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureAppConfiguration((_, config) =>
|
||||||
|
{
|
||||||
|
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- NOWE: async część przed ConfigureServices ---
|
||||||
|
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||||
|
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||||
|
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||||
|
|
||||||
|
var sytelineConnection = await vaultService
|
||||||
|
.GetSecretAsync("database/syteline_prod", "connection_string");
|
||||||
|
var faKrosnoConnection = await vaultService
|
||||||
|
.GetSecretAsync("database/fakrosno", "connection_string");
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
builder.ConfigureServices((_, services) =>
|
||||||
|
{
|
||||||
|
// używamy już gotowego vaultService i connection stringów
|
||||||
|
services.AddSingleton<IVaultService>(vaultService);
|
||||||
|
|
||||||
|
services.AddDbContext<SytelineSaAppDbContext>(
|
||||||
|
options => options.UseSqlServer(sytelineConnection));
|
||||||
|
services.AddDbContext<FaKrosnoDbContext>(
|
||||||
|
options => options.UseSqlServer(faKrosnoConnection));
|
||||||
|
|
||||||
|
services.AddSingleton<IEmailGeneratorService, EmailGeneratorService>();
|
||||||
|
services.AddSingleton<IApp, App>();
|
||||||
|
});
|
||||||
|
|
||||||
|
var builderResult = builder.Build();
|
||||||
|
host = builderResult;
|
||||||
|
|
||||||
|
var vault = host.Services.GetRequiredService<IVaultService>();
|
||||||
|
var syncfusionLicense = await vault.GetSecretAsync("licenses/syncfusion", "license_key", "secret");
|
||||||
|
|
||||||
|
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(syncfusionLicense);
|
||||||
|
|
||||||
|
var app = host.Services.GetRequiredService<IApp>();
|
||||||
|
await app.RunAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
|
||||||
|
if (host == null)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var emailGeneratorService = host.Services.GetRequiredService<IEmailGeneratorService>();
|
||||||
|
var emailModel = new EmailModel
|
||||||
|
{
|
||||||
|
Subject = $"{Assembly.GetEntryAssembly()!.GetName().Name}: Errors encountered during program execution",
|
||||||
|
Body = $"Errors: {e}"
|
||||||
|
};
|
||||||
|
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
||||||
|
await emailGeneratorService.SendEmailAsync(emailModel);
|
||||||
|
}
|
||||||
|
catch (Exception emailException)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error sending error notification email: {emailException}");
|
||||||
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IEmailGeneratorService emailGeneratorService = host.Services.GetRequiredService<IEmailGeneratorService>();
|
// Entry point changed to async Main
|
||||||
|
await MainAsync(args);
|
||||||
EmailModel emailModel = new EmailModel
|
|
||||||
{
|
|
||||||
Subject = $"{Assembly.GetEntryAssembly()!.GetName().Name}: Znaleziono błędy w trakcie wykonywania programu",
|
|
||||||
Body = $"Błędy: {e}"
|
|
||||||
};
|
|
||||||
emailModel.To.Add("piotr.kus@fakrosno.pl");
|
|
||||||
|
|
||||||
emailGeneratorService.SendEmail(emailModel);
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|||||||
11
HeartBeatServer/Properties/launchSettings.json
Normal file
11
HeartBeatServer/Properties/launchSettings.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"HeartBeatServer": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"environmentVariables": {
|
||||||
|
"VAULT_URL": "https://vault.modwad.pl",
|
||||||
|
"VAULT_TOKEN": "hvs.CAESID7fTT61Cs0C37k4pEr98_GXonsWfIgavM0a0nEAb9-qGh4KHGh2cy5DUnNnNjB5Yklla3c3cVNrYmdab01GSzM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +1,97 @@
|
|||||||
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Mail;
|
using System.Net.Mail;
|
||||||
using HeartBeatServers.Models;
|
using System.Threading.Tasks;
|
||||||
|
using HeartBeatServer.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace HeartBeatServers.Services;
|
namespace HeartBeatServer.Services;
|
||||||
|
|
||||||
public class EmailGeneratorService(IConfiguration configuration) : IEmailGeneratorService
|
public class EmailGeneratorService : IEmailGeneratorService, IDisposable
|
||||||
{
|
{
|
||||||
public void SendEmail(EmailModel email)
|
private readonly IConfiguration configuration;
|
||||||
|
private bool disposedValue;
|
||||||
|
|
||||||
|
public EmailGeneratorService(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(EmailModel email)
|
||||||
|
{
|
||||||
|
if (email == null) throw new ArgumentNullException(nameof(email));
|
||||||
|
|
||||||
IConfigurationSection smtpSettings = configuration.GetSection("SmtpSettings");
|
IConfigurationSection smtpSettings = configuration.GetSection("SmtpSettings");
|
||||||
string smtpHost = smtpSettings["Host"] ?? string.Empty;
|
|
||||||
int smtpPort = int.Parse(smtpSettings["Port"] ?? "0");
|
if (smtpSettings == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SMTP settings are not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string smtpHost = smtpSettings["Host"] ?? throw new ArgumentException("SMTP host is not configured.");
|
||||||
|
|
||||||
|
if (!int.TryParse(smtpSettings["Port"], out int smtpPort) || smtpPort <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("SMTP port is invalid or not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
string smtpUsername = smtpSettings["Username"] ?? string.Empty;
|
string smtpUsername = smtpSettings["Username"] ?? string.Empty;
|
||||||
string smtpPassword = smtpSettings["Password"] ?? string.Empty;
|
string smtpPassword = smtpSettings["Password"] ?? string.Empty;
|
||||||
string fromEmail = smtpSettings["FromEmail"] ?? string.Empty;
|
string fromEmail = smtpSettings["FromEmail"] ?? throw new ArgumentException("FromEmail is not configured.");
|
||||||
|
|
||||||
using var mailMessage = new MailMessage();
|
using var mailMessage = new MailMessage()
|
||||||
mailMessage.From = new MailAddress(fromEmail);
|
{
|
||||||
mailMessage.Subject = email.Subject;
|
From = new MailAddress(fromEmail),
|
||||||
mailMessage.Body = email.Body;
|
Subject = email.Subject ?? string.Empty,
|
||||||
mailMessage.IsBodyHtml = false;
|
Body = email.Body ?? string.Empty,
|
||||||
|
IsBodyHtml = false
|
||||||
|
};
|
||||||
|
|
||||||
foreach (string attachment in email.Attachments)
|
foreach (string attachment in email.Attachments)
|
||||||
{
|
{
|
||||||
mailMessage.Attachments.Add(new Attachment(attachment));
|
if (!string.IsNullOrWhiteSpace(attachment))
|
||||||
|
mailMessage.Attachments.Add(new Attachment(attachment));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string toAddress in email.To)
|
foreach (string toAddress in email.To)
|
||||||
{
|
{
|
||||||
mailMessage.To.Add(toAddress);
|
if (!string.IsNullOrWhiteSpace(toAddress))
|
||||||
|
mailMessage.To.Add(toAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string ccAddress in email.Cc)
|
foreach (string ccAddress in email.Cc)
|
||||||
{
|
{
|
||||||
mailMessage.CC.Add(ccAddress);
|
if (!string.IsNullOrWhiteSpace(ccAddress))
|
||||||
|
mailMessage.CC.Add(ccAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (string bccAddress in email.Bcc)
|
foreach (string bccAddress in email.Bcc)
|
||||||
{
|
{
|
||||||
mailMessage.Bcc.Add(bccAddress);
|
if (!string.IsNullOrWhiteSpace(bccAddress))
|
||||||
|
mailMessage.Bcc.Add(bccAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var smtpClient = new SmtpClient(smtpHost, smtpPort);
|
using var smtpClient = new SmtpClient(smtpHost, smtpPort)
|
||||||
smtpClient.EnableSsl = true;
|
{
|
||||||
smtpClient.Credentials = new NetworkCredential(smtpUsername, smtpPassword);
|
EnableSsl = true,
|
||||||
smtpClient.Send(mailMessage);
|
Credentials = new NetworkCredential(smtpUsername, smtpPassword)
|
||||||
|
};
|
||||||
|
|
||||||
|
await smtpClient.SendMailAsync(mailMessage);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposedValue)
|
||||||
|
{
|
||||||
|
// No unmanaged resources besides smtpClient which is scoped locally now
|
||||||
|
disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using HeartBeatServers.Models;
|
using HeartBeatServer.Models;
|
||||||
|
|
||||||
namespace HeartBeatServers.Services;
|
namespace HeartBeatServer.Services;
|
||||||
|
|
||||||
public interface IEmailGeneratorService
|
public interface IEmailGeneratorService
|
||||||
{
|
{
|
||||||
void SendEmail(EmailModel email);
|
Task SendEmailAsync(EmailModel email);
|
||||||
}
|
}
|
||||||
6
HeartBeatServer/Services/ISmtpClientWrapperService.cs
Normal file
6
HeartBeatServer/Services/ISmtpClientWrapperService.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HeartBeatServer.Services;
|
||||||
|
|
||||||
|
public interface ISmtpClientWrapperService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
6
HeartBeatServer/Services/SmtpClientWrapperService.cs
Normal file
6
HeartBeatServer/Services/SmtpClientWrapperService.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace HeartBeatServer.Services;
|
||||||
|
|
||||||
|
public class SmtpClientWrapperService : ISmtpClientWrapperService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,136 +1,189 @@
|
|||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Gitea.Net.Api;
|
using Gitea.Net.Api;
|
||||||
using Gitea.Net.Client;
|
|
||||||
using Gitea.Net.Model;
|
using Gitea.Net.Model;
|
||||||
using OpenAI;
|
using PipelineAgent.ChangesChecker.Models;
|
||||||
using OpenAI.Chat;
|
using Services.Gitea;
|
||||||
|
using Services.OpenAI;
|
||||||
using Services.Vault;
|
using Services.Vault;
|
||||||
|
|
||||||
namespace PipelineAgent.ChangesChecker;
|
namespace PipelineAgent.ChangesChecker;
|
||||||
|
|
||||||
public class ChangesCheckerAgent
|
public class ChangesCheckerAgent(IOpenAiService openAiService, IVaultService vaultService, IGiteaService giteaService)
|
||||||
|
: IChangesCheckerAgent
|
||||||
{
|
{
|
||||||
public async Task<int> CheckChangesAsync()
|
public async Task<int> CheckChangesAsync(string repositoryPath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<string> lastChangesFromGitea = await GetLastChangesFromGitea();
|
var giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token_write", "secret")
|
||||||
|
.ConfigureAwait(false);
|
||||||
var userContent = JsonSerializer.Serialize(lastChangesFromGitea);
|
var giteaConfiguration = GetGiteaConfiguration(repositoryPath, giteaApiToken);
|
||||||
|
var giteaClient = giteaService.CreateGiteaClient(giteaConfiguration);
|
||||||
|
var lastChangesFromGitea =
|
||||||
|
await GetLastChangesFromGiteaAsync(giteaClient, giteaConfiguration).ConfigureAwait(false);
|
||||||
|
|
||||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
|
if (lastChangesFromGitea.Count == 0)
|
||||||
if (string.IsNullOrWhiteSpace(apiKey))
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
return 0;
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = new OpenAIClient(apiKey);
|
var chatRequest = lastChangesFromGitea.Select(x => new ChatRequest(x.Value.Item1, x.Key, x.Value.Item2));
|
||||||
|
var promptPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts", "ChangesChecker.txt");
|
||||||
|
var prompt = await File.ReadAllTextAsync(promptPath).ConfigureAwait(false);
|
||||||
|
|
||||||
var systemPrompt = """
|
var decisionDoc = await openAiService.GetResponseFromChat(chatRequest, prompt).ConfigureAwait(false);
|
||||||
Jesteś Senior .NET Developerem z kilkunastoletnim stażem.
|
|
||||||
Na podstawie listy zawartości plików zmienionych w ostatnich commitach:
|
|
||||||
|
|
||||||
- zdekoduj zawartość pliku zapisaną w base64
|
if (decisionDoc == null)
|
||||||
- sprwadź kod pod kątem poprawności i potencjalnych błędów
|
{
|
||||||
- zasugeruj poprawki lub ulepszenia, jeśli to konieczne, tak, żeby kod był jak najbardziej optymalny i profesjonalny
|
Console.WriteLine("AI–Gate: no response from LLM, continuing by default.");
|
||||||
- zwróć JEDEN obiekt JSON w formacie:
|
return 0;
|
||||||
{
|
}
|
||||||
"file-content": "zdekodowana zawartość pliku",
|
|
||||||
"improvements": "wprowadzone poprawki w kodzie lub 'brak' w jezyku polskim",
|
|
||||||
"changed-file" "nowy plik z wprowadzonymi poprawkami lub 'brak'",
|
|
||||||
"decision": "approve" | "changes-requested"
|
|
||||||
}
|
|
||||||
|
|
||||||
Decyzja:
|
if (!decisionDoc.RootElement.TryGetProperty("decision", out var decisionElement) ||
|
||||||
- "approve" gdy kod jest w porzadku i nie potrzebuje zadnych zmian.
|
decisionElement.ValueKind == JsonValueKind.Null)
|
||||||
- "changes-requested" gdy kod wymaga poprawek lub ulepszeń.
|
{
|
||||||
Nie dodawaj żadnego tekstu poza JSON.
|
Console.WriteLine("AI–Gate: decision property missing or null in LLM response, continuing by default.");
|
||||||
""";
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// var userContent = JsonSerializer.Serialize(failuresForModel);
|
if (!decisionDoc.RootElement.TryGetProperty("changes", out var changesElement) ||
|
||||||
//
|
changesElement.ValueKind == JsonValueKind.Null)
|
||||||
var chat = client.GetChatClient("gpt-4.1-mini");
|
{
|
||||||
|
return 0;
|
||||||
var response = await chat.CompleteChatAsync(new SystemChatMessage(systemPrompt),
|
}
|
||||||
new UserChatMessage($"Pliki ostatnio zmienione:\n{userContent}"));
|
|
||||||
|
|
||||||
var json = response.Value.Content[0].Text;
|
|
||||||
Console.WriteLine("AI‑Gate LLM raw response:");
|
|
||||||
Console.WriteLine(json);
|
|
||||||
|
|
||||||
var decisionDoc = JsonDocument.Parse(json);
|
|
||||||
var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
|
|
||||||
var improvements = decisionDoc.RootElement.GetProperty("improvements").GetString();
|
|
||||||
var fileContent = decisionDoc.RootElement.GetProperty("file-content").GetString();
|
|
||||||
var changedFile = decisionDoc.RootElement.GetProperty("changed-file").GetString();
|
|
||||||
|
|
||||||
Console.WriteLine($"AI‑Gate decision: {decision}");
|
|
||||||
Console.WriteLine($"Improvements: {improvements}");
|
|
||||||
Console.WriteLine($"FileContent: {fileContent}");
|
|
||||||
Console.WriteLine($"ChangedFile: {changedFile}");
|
|
||||||
//
|
|
||||||
// return decision switch
|
|
||||||
// {
|
|
||||||
// "allow" => 0,
|
|
||||||
// "allow-with-warning" => 0,
|
|
||||||
// _ => 1
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
var content = GetFormattedJsonRequest(changesElement);
|
||||||
|
var (status, response) = await giteaService.SendRequestAsync(giteaConfiguration, "contents", content)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (status)
|
||||||
|
{
|
||||||
|
Console.WriteLine("AI–Gate: created PR with suggested changes.");
|
||||||
|
Console.WriteLine(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("AI–Gate: failed to create PR with suggested changes.");
|
||||||
|
Console.WriteLine(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Console.WriteLine("AI–Gate: operation was canceled.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AI‑Gate: error while reading TRX: {ex.Message}");
|
Console.WriteLine($"AI–Gate: while processing: {ex.Message}");
|
||||||
return 1;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<string>> GetLastChangesFromGitea()
|
private async Task<Dictionary<string, (string, string)>> GetLastChangesFromGiteaAsync(RepositoryApi giteaClient,
|
||||||
|
GiteaConfiguration giteaConfiguration)
|
||||||
{
|
{
|
||||||
var lastChanges = new List<string>();
|
var lastChanges = new Dictionary<string, (string, string)>(StringComparer.Ordinal);
|
||||||
|
var lastUpdatedBranch =
|
||||||
|
(await giteaService.GetAllBranchesAsync(giteaClient, giteaConfiguration).ConfigureAwait(false))
|
||||||
|
.Where(x => x.Name != "master" && x.Name.StartsWith("feature/")).OrderByDescending(x => x.Commit.Timestamp)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
RepositoryApi repositoryApi = await CreateGiteaClient();
|
if (lastUpdatedBranch == null ||
|
||||||
|
lastUpdatedBranch.Commit.Message.Contains("LLM: Code review suggestions", StringComparison.Ordinal))
|
||||||
var lastCommits = await repositoryApi.RepoGetAllCommitsAsync("FA", "FA", "master", limit: 2);
|
|
||||||
var commitAffectedFilesList = lastCommits.SelectMany(x => x.Files).ToList();
|
|
||||||
|
|
||||||
foreach (CommitAffectedFiles commitAffectedFile in commitAffectedFilesList)
|
|
||||||
{
|
{
|
||||||
var repoGetContentsAsync =
|
return lastChanges;
|
||||||
await repositoryApi.RepoGetContentsAsync("FA", "FA", commitAffectedFile.Filename);
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(repoGetContentsAsync.Content))
|
var lastCommit = await giteaService
|
||||||
|
.GetCommitByIdAsync(giteaClient, giteaConfiguration, lastUpdatedBranch.Commit.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (lastCommit == null)
|
||||||
|
{
|
||||||
|
return lastChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (CommitAffectedFiles commitAffectedFile in lastCommit.Files)
|
||||||
|
{
|
||||||
|
var fileContent = await giteaService
|
||||||
|
.GetFileContentAsync(giteaClient, giteaConfiguration, commitAffectedFile.Filename,
|
||||||
|
lastUpdatedBranch.Name).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(fileContent.Content) && !lastChanges.ContainsKey(commitAffectedFile.Filename))
|
||||||
{
|
{
|
||||||
lastChanges.Add(repoGetContentsAsync.Content);
|
lastChanges.Add(commitAffectedFile.Filename, (lastUpdatedBranch.Name, Base64Decode(fileContent.Content)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastChanges;
|
return lastChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VaultService CreateVaultService()
|
private GiteaConfiguration GetGiteaConfiguration(string repositoryPath, string giteaApiToken)
|
||||||
{
|
{
|
||||||
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
ReadOnlySpan<char> repoSpan = repositoryPath.AsSpan();
|
||||||
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
int lastSlash = repoSpan.LastIndexOf('/');
|
||||||
return new VaultService(vaultUrl, vaultToken);
|
int secondLastSlash = lastSlash > 0 ? repoSpan.Slice(0, lastSlash).LastIndexOf('/') : -1;
|
||||||
|
|
||||||
|
string owner = secondLastSlash >= 0
|
||||||
|
? repoSpan.Slice(secondLastSlash + 1, lastSlash - secondLastSlash - 1).ToString()
|
||||||
|
: string.Empty;
|
||||||
|
string repository = lastSlash >= 0 ? repoSpan.Slice(lastSlash + 1).ToString() : repositoryPath;
|
||||||
|
const string branch = "master";
|
||||||
|
const string host = "https://git.modwad.pl";
|
||||||
|
|
||||||
|
return new GiteaConfiguration
|
||||||
|
{
|
||||||
|
Owner = owner,
|
||||||
|
Repository = repository,
|
||||||
|
Branch = branch,
|
||||||
|
ApiToken = giteaApiToken,
|
||||||
|
Host = host
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<RepositoryApi> CreateGiteaClient()
|
private string Base64Decode(string base64EncodedData)
|
||||||
{
|
{
|
||||||
VaultService vaultService = CreateVaultService();
|
byte[] bytes = Convert.FromBase64String(base64EncodedData);
|
||||||
string giteaApiToken = await vaultService.GetSecretAsync("api_keys/gitea", "gitea_api_token", "secret") ??
|
return Encoding.UTF8.GetString(bytes);
|
||||||
string.Empty;
|
}
|
||||||
|
|
||||||
var config = new Configuration
|
private StringContent GetFormattedJsonRequest(JsonElement json)
|
||||||
|
{
|
||||||
|
var branch = json.GetProperty("branch").GetString();
|
||||||
|
var newBranch = json.GetProperty("new_branch").GetString();
|
||||||
|
var message = json.GetProperty("message").GetString();
|
||||||
|
|
||||||
|
var filesJson = json.GetProperty("files");
|
||||||
|
var files = new List<object>(filesJson.GetArrayLength());
|
||||||
|
|
||||||
|
foreach (var fileEl in filesJson.EnumerateArray())
|
||||||
{
|
{
|
||||||
BasePath = "https://git.modwad.pl/api/v1",
|
var operation = fileEl.GetProperty("operation").GetString();
|
||||||
ApiKey =
|
var path = fileEl.GetProperty("path").GetString();
|
||||||
|
var contentPlain = fileEl.GetProperty("content").GetString() ?? string.Empty;
|
||||||
|
|
||||||
|
var contentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(contentPlain));
|
||||||
|
|
||||||
|
files.Add(new
|
||||||
{
|
{
|
||||||
["token"] = giteaApiToken
|
operation,
|
||||||
}
|
path,
|
||||||
|
content = contentBase64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
branch,
|
||||||
|
new_branch = newBranch,
|
||||||
|
message,
|
||||||
|
files
|
||||||
};
|
};
|
||||||
|
|
||||||
return new RepositoryApi(config);
|
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||||
|
|
||||||
|
return new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
PipelineAgent/ChangesChecker/IChangesCheckerAgent.cs
Normal file
6
PipelineAgent/ChangesChecker/IChangesCheckerAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace PipelineAgent.ChangesChecker;
|
||||||
|
|
||||||
|
public interface IChangesCheckerAgent
|
||||||
|
{
|
||||||
|
Task<int> CheckChangesAsync(string repositoryPath);
|
||||||
|
}
|
||||||
3
PipelineAgent/ChangesChecker/Models/ChatRequest.cs
Normal file
3
PipelineAgent/ChangesChecker/Models/ChatRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace PipelineAgent.ChangesChecker.Models;
|
||||||
|
|
||||||
|
public record ChatRequest(string branchName, string FileName, string FileContent);
|
||||||
@@ -9,8 +9,18 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Gitea.Net.API" Version="25.8.18" />
|
<PackageReference Include="Gitea.Net.API" Version="25.8.18" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.2.25163.2" />
|
||||||
<PackageReference Include="OpenAI" Version="2.8.0" />
|
<PackageReference Include="OpenAI" Version="2.8.0" />
|
||||||
<PackageReference Include="Services" Version="2.0.0-alpha.0" />
|
<PackageReference Include="Services" Version="2.0.0-alpha.0.154" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Prompts\TestsChecker.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Prompts\ChangesChecker.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,35 +1,38 @@
|
|||||||
using System.Text.Json;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Xml.Linq;
|
using Microsoft.Extensions.Hosting;
|
||||||
using OpenAI;
|
|
||||||
using OpenAI.Chat;
|
|
||||||
using PipelineAgent.ChangesChecker;
|
using PipelineAgent.ChangesChecker;
|
||||||
using PipelineAgent.TestsChecker;
|
using PipelineAgent.TestsChecker;
|
||||||
|
using Services.Gitea;
|
||||||
|
using Services.OpenAI;
|
||||||
|
using Services.Vault;
|
||||||
|
|
||||||
|
IHost builder = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureServices((_, services) =>
|
||||||
|
{
|
||||||
|
var vaultUrl = Environment.GetEnvironmentVariable("VAULT_URL") ?? "http://vault:8200";
|
||||||
|
var vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN") ?? "dev-only-token";
|
||||||
|
var vaultService = new VaultService(vaultUrl, vaultToken);
|
||||||
|
|
||||||
|
services.AddSingleton<IVaultService>(vaultService);
|
||||||
|
|
||||||
|
services.AddScoped<IGiteaService, GiteaService>();
|
||||||
|
services.AddScoped<IOpenAiService, OpenAiService>();
|
||||||
|
services.AddScoped<ITestsCheckerAgent, TestsCheckerAgent>();
|
||||||
|
services.AddScoped<IChangesCheckerAgent, ChangesCheckerAgent>();
|
||||||
|
}).Build();
|
||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Usage: CiAgent <command> [options]");
|
Console.WriteLine("Usage: CiAgent <command> [options]");
|
||||||
Console.WriteLine("Commands: test-gate, container-gate, deploy-gate");
|
Console.WriteLine("Commands: test-gate, check-changes, deploy-gate");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var command = args[0];
|
return args[0] switch
|
||||||
var commandArgs = args.Skip(1).ToArray();
|
|
||||||
|
|
||||||
return command switch
|
|
||||||
{
|
{
|
||||||
"test-gate" => await RunTestGateAsync(commandArgs[0]),
|
"test-gate" => await builder.Services.GetRequiredService<ITestsCheckerAgent>()
|
||||||
"check-changes" => await RunChangesGateAsync(),
|
.CheckTestsAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||||
|
"check-changes" => await builder.Services.GetRequiredService<IChangesCheckerAgent>()
|
||||||
|
.CheckChangesAsync(args.ElementAtOrDefault(1) ?? string.Empty),
|
||||||
_ => 1
|
_ => 1
|
||||||
};
|
};
|
||||||
|
|
||||||
static async Task<int> RunTestGateAsync(string arg)
|
|
||||||
{
|
|
||||||
var testsCheckerAgent = new TestsCheckerAgent();
|
|
||||||
return await testsCheckerAgent.CheckTestsAsync(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async Task<int> RunChangesGateAsync()
|
|
||||||
{
|
|
||||||
var changesCheckerAgent = new ChangesCheckerAgent();
|
|
||||||
return await changesCheckerAgent.CheckChangesAsync();
|
|
||||||
}
|
|
||||||
38
PipelineAgent/Prompts/ChangesChecker.txt
Normal file
38
PipelineAgent/Prompts/ChangesChecker.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
Jesteś Senior .NET Developerem z kilkunastoletnim stażem.
|
||||||
|
Programujesz w .NET 8 i C# 12, znasz najlepsze praktyki programistyczne oraz najnowoczesniejsza konwencje.
|
||||||
|
Na podstawie listy zawartości plików zmienionych w ostatnich commitach:
|
||||||
|
|
||||||
|
- sprwadź kod pod kątem poprawności i potencjalnych błędów
|
||||||
|
- zasugeruj poprawki lub ulepszenia, jeśli to konieczne, tak, żeby kod był jak najbardziej optymalny i profesjonalny
|
||||||
|
- zwróć JEDEN obiekt JSON w formacie:
|
||||||
|
{
|
||||||
|
decision: "approve" | "changes-requested",
|
||||||
|
changes:
|
||||||
|
{
|
||||||
|
branch = "branchName podany w danych wejsciowych branch lub main/master",
|
||||||
|
new_branch = "code-review_<losowy_ciag_znakow>",
|
||||||
|
message = "LLM: Code review suggestions plus to co zmieniono wypisane w liscie (*) po angielsku",
|
||||||
|
files = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
operation = "update",
|
||||||
|
path = nazwa pliku z requesta,
|
||||||
|
content = poprawiony plik
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
- tworz changes tylko wtedy jesli sugerujesz jakies zmiany w kodzie
|
||||||
|
- sprawdzaj, czy sugerowane zmiany nie wprowadzają nowych błędów lub problemów
|
||||||
|
- upewnij się, że sugerowane zmiany są zgodne z najlepszymi praktykami programistycznymi
|
||||||
|
- jesli klasa dziedziczy po interfejsie lub klasie bazowej, nie zmieniaj nazw metod lub ich sygnatur
|
||||||
|
- dostarczaj jasne i zwięzłe uzasadnienie dla każdej sugerowanej zmiany
|
||||||
|
- unikaj sugerowania zmian, które są zbędne lub nieistotne
|
||||||
|
- pamiętaj, że Twoim celem jest poprawa jakości kodu
|
||||||
|
- sprawdzaj wygenerowany json pod kątem poprawności składniowej i kompletności przed zwróceniem go użytkownikowi
|
||||||
|
- jesli nie sugerujesz żadnych zmian w kodzie, pole "changes" powinno być puste (null)
|
||||||
|
Decyzja:
|
||||||
|
- "approve" gdy kod jest w porzadku i nie potrzebuje zadnych zmian (pole changes jest null).
|
||||||
|
- "changes-requested" gdy kod wymaga poprawek lub ulepszeń.
|
||||||
|
Nie dodawaj żadnego tekstu poza JSON.
|
||||||
13
PipelineAgent/Prompts/TestsChecker.txt
Normal file
13
PipelineAgent/Prompts/TestsChecker.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Jesteś asystentem DevOps oceniającym wyniki testów .NET.
|
||||||
|
Na podstawie listy błędów:
|
||||||
|
- sklasyfikuj każdy błąd jako: CriticalBug, InfraOrConfig, FlakyTest
|
||||||
|
- zwróć JEDEN obiekt JSON w formacie:
|
||||||
|
{
|
||||||
|
"decision": "block" | "allow" | "allow-with-warning",
|
||||||
|
"reason": "krótkie wyjaśnienie po polsku"
|
||||||
|
}
|
||||||
|
Decyzja:
|
||||||
|
- "block" gdy jest choć jeden CriticalBug.
|
||||||
|
- "allow-with-warning" gdy są tylko InfraOrConfig lub FlakyTest, ale wygląda to na coś, co trzeba sprawdzić.
|
||||||
|
- "allow" gdy wszystko wskazuje na drobne lub znane flaky testy.
|
||||||
|
Nie dodawaj żadnego tekstu poza JSON.
|
||||||
6
PipelineAgent/TestsChecker/ITestsCheckerAgent.cs
Normal file
6
PipelineAgent/TestsChecker/ITestsCheckerAgent.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace PipelineAgent.TestsChecker;
|
||||||
|
|
||||||
|
public interface ITestsCheckerAgent
|
||||||
|
{
|
||||||
|
Task<int> CheckTestsAsync(string trxPath);
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using OpenAI;
|
using Services.OpenAI;
|
||||||
using OpenAI.Chat;
|
|
||||||
|
|
||||||
namespace PipelineAgent.TestsChecker;
|
namespace PipelineAgent.TestsChecker;
|
||||||
|
|
||||||
public class TestsCheckerAgent
|
public class TestsCheckerAgent(IOpenAiService openAiService) : ITestsCheckerAgent
|
||||||
{
|
{
|
||||||
public async Task<int> CheckTestsAsync(string trxPath)
|
public async Task<int> CheckTestsAsync(string trxPath)
|
||||||
{
|
{
|
||||||
@@ -18,19 +16,11 @@ public class TestsCheckerAgent
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
XDocument doc = XDocument.Load(trxPath);
|
XDocument doc = XDocument.Load(trxPath);
|
||||||
|
|
||||||
// Standardowy namespace MSTest/TRX
|
|
||||||
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
|
XNamespace ns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";
|
||||||
|
|
||||||
var failed = doc
|
var failed = doc.Descendants(ns + "UnitTestResult").Where(x => (string?)x.Attribute("outcome") == "Failed")
|
||||||
.Descendants(ns + "UnitTestResult")
|
|
||||||
.Where(x => (string?)x.Attribute("outcome") == "Failed")
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
var total = doc.Descendants(ns + "UnitTestResult").Count();
|
||||||
var total = doc
|
|
||||||
.Descendants(ns + "UnitTestResult")
|
|
||||||
.Count();
|
|
||||||
|
|
||||||
var failuresForModel = failed.Select(f => new
|
var failuresForModel = failed.Select(f => new
|
||||||
{
|
{
|
||||||
TestName = (string?)f.Attribute("testName") ?? "<no name>",
|
TestName = (string?)f.Attribute("testName") ?? "<no name>",
|
||||||
@@ -41,48 +31,21 @@ public class TestsCheckerAgent
|
|||||||
.Value
|
.Value
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
|
var prompt = await File.ReadAllTextAsync(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Prompts",
|
||||||
if (string.IsNullOrWhiteSpace(apiKey))
|
"TestsChecker.txt"));
|
||||||
|
|
||||||
|
var decisionDoc = await openAiService.GetResponseFromChat(failuresForModel, prompt);
|
||||||
|
|
||||||
|
if (decisionDoc == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
Console.WriteLine("AI‑Gate: no response from LLM, blocking by default.");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = new OpenAIClient(apiKey);
|
|
||||||
|
|
||||||
var systemPrompt = """
|
|
||||||
Jesteś asystentem DevOps oceniającym wyniki testów .NET.
|
|
||||||
Na podstawie listy błędów:
|
|
||||||
|
|
||||||
- sklasyfikuj każdy błąd jako: CriticalBug, InfraOrConfig, FlakyTest
|
|
||||||
- zwróć JEDEN obiekt JSON w formacie:
|
|
||||||
{
|
|
||||||
"decision": "block" | "allow" | "allow-with-warning",
|
|
||||||
"reason": "krótkie wyjaśnienie po polsku"
|
|
||||||
}
|
|
||||||
|
|
||||||
Decyzja:
|
|
||||||
- "block" gdy jest choć jeden CriticalBug.
|
|
||||||
- "allow-with-warning" gdy są tylko InfraOrConfig lub FlakyTest, ale wygląda to na coś, co trzeba sprawdzić.
|
|
||||||
- "allow" gdy wszystko wskazuje na drobne lub znane flaky testy.
|
|
||||||
Nie dodawaj żadnego tekstu poza JSON.
|
|
||||||
""";
|
|
||||||
|
|
||||||
var userContent = JsonSerializer.Serialize(failuresForModel);
|
|
||||||
|
|
||||||
var chat = client.GetChatClient("gpt-4.1-mini");
|
|
||||||
|
|
||||||
var response = await chat.CompleteChatAsync(new SystemChatMessage(systemPrompt),
|
|
||||||
new UserChatMessage($"Błędy testów:\n{userContent}"));
|
|
||||||
|
|
||||||
var json = response.Value.Content[0].Text;
|
|
||||||
Console.WriteLine("AI‑Gate LLM raw response:");
|
|
||||||
Console.WriteLine(json);
|
|
||||||
|
|
||||||
var decisionDoc = JsonDocument.Parse(json);
|
|
||||||
var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
|
var decision = decisionDoc.RootElement.GetProperty("decision").GetString();
|
||||||
var reason = decisionDoc.RootElement.GetProperty("reason").GetString();
|
var reason = decisionDoc.RootElement.GetProperty("reason").GetString();
|
||||||
|
|
||||||
|
Console.WriteLine($"Total: {total}, Failed: {failed.Count}");
|
||||||
Console.WriteLine($"AI‑Gate decision: {decision}");
|
Console.WriteLine($"AI‑Gate decision: {decision}");
|
||||||
Console.WriteLine($"Reason: {reason}");
|
Console.WriteLine($"Reason: {reason}");
|
||||||
|
|
||||||
|
|||||||
13
Services/Gitea/GiteaConfiguration.cs
Normal file
13
Services/Gitea/GiteaConfiguration.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Services.Gitea;
|
||||||
|
|
||||||
|
public class GiteaConfiguration
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = string.Empty;
|
||||||
|
public string Owner { get; set; } = string.Empty;
|
||||||
|
public string Repository { get; set; } = string.Empty;
|
||||||
|
public string Branch { get; set; } = string.Empty;
|
||||||
|
public string ApiToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Url => $"{Host}/api/v1/repos/{Owner}/{Repository}";
|
||||||
|
public string BaseUrl => $"{Host}/api/v1";
|
||||||
|
}
|
||||||
114
Services/Gitea/GiteaService.cs
Normal file
114
Services/Gitea/GiteaService.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using Gitea.Net.Api;
|
||||||
|
using Gitea.Net.Client;
|
||||||
|
using Gitea.Net.Model;
|
||||||
|
using HttpMethod = System.Net.Http.HttpMethod;
|
||||||
|
|
||||||
|
namespace Services.Gitea;
|
||||||
|
|
||||||
|
public class GiteaService : IGiteaService
|
||||||
|
{
|
||||||
|
private static readonly HttpClient HttpClient = new();
|
||||||
|
|
||||||
|
public RepositoryApi CreateGiteaClient(GiteaConfiguration configuration)
|
||||||
|
{
|
||||||
|
if (configuration == null)
|
||||||
|
throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
||||||
|
var config = new Configuration
|
||||||
|
{
|
||||||
|
BasePath = configuration.BaseUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
config.ApiKey = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["token"] = configuration.ApiToken
|
||||||
|
};
|
||||||
|
|
||||||
|
return new RepositoryApi(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Commit>> GetLastCommitsAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||||
|
int limit)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
if (limit <= 0) throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be greater than zero.");
|
||||||
|
|
||||||
|
var lastCommits = await repositoryApi.RepoGetAllCommitsAsync(configuration.Owner, configuration.Repository,
|
||||||
|
configuration.Branch, limit: limit);
|
||||||
|
return lastCommits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Commit?> GetLastCommitAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
|
||||||
|
var lastCommit = await GetLastCommitsAsync(repositoryApi, configuration, 1);
|
||||||
|
return lastCommit.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Commit?> GetCommitByIdAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||||
|
string commitId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
if (string.IsNullOrEmpty(commitId))
|
||||||
|
throw new ArgumentException("CommitId cannot be null or empty.", nameof(commitId));
|
||||||
|
|
||||||
|
var lastCommit =
|
||||||
|
await repositoryApi.RepoGetSingleCommitAsync(configuration.Owner, configuration.Repository, commitId);
|
||||||
|
return lastCommit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Branch>> GetAllBranchesAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
|
||||||
|
var branches = await repositoryApi.RepoListBranchesAsync(configuration.Owner, configuration.Repository);
|
||||||
|
return branches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ContentsResponse> GetFileContentAsync(RepositoryApi repositoryApi,
|
||||||
|
GiteaConfiguration configuration, string filename, string branch = "master")
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(repositoryApi);
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
if (string.IsNullOrEmpty(filename))
|
||||||
|
throw new ArgumentException("Filename cannot be null or empty.", nameof(filename));
|
||||||
|
|
||||||
|
var repoGetContentsAsync = await repositoryApi.RepoGetContentsAsync(configuration.Owner,
|
||||||
|
configuration.Repository, filename, varRef: branch);
|
||||||
|
return repoGetContentsAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Status, string Response)> SendRequestAsync(GiteaConfiguration configuration,
|
||||||
|
string endpoint, HttpContent content)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
ArgumentNullException.ThrowIfNull(content);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(endpoint))
|
||||||
|
throw new ArgumentException("Endpoint cannot be null or empty.", nameof(endpoint));
|
||||||
|
|
||||||
|
var requestUrl = $"{configuration.Url}/{endpoint}";
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
|
||||||
|
request.Content = content;
|
||||||
|
|
||||||
|
request.Headers.Authorization =
|
||||||
|
new System.Net.Http.Headers.AuthenticationHeaderValue("token", configuration.ApiToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var giteaResponse = await HttpClient.SendAsync(request);
|
||||||
|
var response = await giteaResponse.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
return (giteaResponse.IsSuccessStatusCode, response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error while sending request to Gitea: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Services/Gitea/IGiteaService.cs
Normal file
22
Services/Gitea/IGiteaService.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Gitea.Net.Api;
|
||||||
|
using Gitea.Net.Model;
|
||||||
|
|
||||||
|
namespace Services.Gitea;
|
||||||
|
|
||||||
|
public interface IGiteaService
|
||||||
|
{
|
||||||
|
RepositoryApi CreateGiteaClient(GiteaConfiguration configuration);
|
||||||
|
Task<List<Commit>> GetLastCommitsAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration, int limit);
|
||||||
|
|
||||||
|
Task<ContentsResponse> GetFileContentAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration,
|
||||||
|
string filename, string branch = "master");
|
||||||
|
|
||||||
|
Task<Commit?> GetCommitByIdAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration, string commitId);
|
||||||
|
|
||||||
|
Task<Commit?> GetLastCommitAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration);
|
||||||
|
|
||||||
|
Task<List<Branch>> GetAllBranchesAsync(RepositoryApi repositoryApi, GiteaConfiguration configuration);
|
||||||
|
|
||||||
|
Task<(bool Status, string Response)> SendRequestAsync(GiteaConfiguration configuration, string endpoint,
|
||||||
|
HttpContent content);
|
||||||
|
}
|
||||||
8
Services/OpenAI/IOpenAIService.cs
Normal file
8
Services/OpenAI/IOpenAIService.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Services.OpenAI;
|
||||||
|
|
||||||
|
public interface IOpenAiService
|
||||||
|
{
|
||||||
|
Task<JsonDocument?> GetResponseFromChat(object request, string prompt, string model = "gpt-4.1-mini");
|
||||||
|
}
|
||||||
32
Services/OpenAI/OpenAiService.cs
Normal file
32
Services/OpenAI/OpenAiService.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using OpenAI;
|
||||||
|
using OpenAI.Chat;
|
||||||
|
using Services.Vault;
|
||||||
|
|
||||||
|
namespace Services.OpenAI;
|
||||||
|
|
||||||
|
public class OpenAiService(IVaultService vaultService) : IOpenAiService
|
||||||
|
{
|
||||||
|
public async Task<JsonDocument?> GetResponseFromChat(object request, string prompt, string model = "gpt-4.1-mini")
|
||||||
|
{
|
||||||
|
var apiKey = await vaultService.GetSecretAsync("api_keys/openai", "openai_api_key", "secret");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
Console.WriteLine("AI‑Gate: OPENAI_API_KEY not set, blocking by default.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = new OpenAIClient(apiKey);
|
||||||
|
var userContent = JsonSerializer.Serialize(request);
|
||||||
|
var chat = client.GetChatClient(model);
|
||||||
|
var response = await chat.CompleteChatAsync(new SystemChatMessage(prompt),
|
||||||
|
new UserChatMessage($"{userContent}"));
|
||||||
|
var json = response.Value.Content[0].Text;
|
||||||
|
|
||||||
|
Console.WriteLine("AI‑Gate LLM raw response:");
|
||||||
|
Console.WriteLine(json);
|
||||||
|
|
||||||
|
return JsonDocument.Parse(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,18 @@
|
|||||||
<Authors>Piotr Kus</Authors>
|
<Authors>Piotr Kus</Authors>
|
||||||
<Description>Service for communication with Vault</Description>
|
<Description>Service for communication with Vault</Description>
|
||||||
<MinVerAutoIncrement>patch</MinVerAutoIncrement>
|
<MinVerAutoIncrement>patch</MinVerAutoIncrement>
|
||||||
<MinVerMinimumMajorMinor>1.0</MinVerMinimumMajorMinor>
|
<MinVerMinimumMajorMinor>2.0</MinVerMinimumMajorMinor>
|
||||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="VaultSharp" Version="1.17.5.1" />
|
<PackageReference Include="Gitea.Net.API" Version="25.8.18" />
|
||||||
|
<PackageReference Include="OpenAI" Version="2.8.0"/>
|
||||||
|
<PackageReference Include="VaultSharp" Version="1.17.5.1"/>
|
||||||
|
<PackageReference Include="MinVer" Version="6.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
46
Tests/tests.trx
Normal file
46
Tests/tests.trx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TestRun id="a459252d-fde3-4bb0-9b86-00a2c046526b" name="@Mac 2026-01-20 05:32:45" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
|
||||||
|
<Times creation="2026-01-20T05:32:45.9495100+01:00" queuing="2026-01-20T05:32:45.9495100+01:00" start="2026-01-20T05:32:45.6832770+01:00" finish="2026-01-20T05:32:45.9519250+01:00" />
|
||||||
|
<TestSettings name="default" id="bb142d87-2085-458c-ae53-1afd077420df">
|
||||||
|
<Deployment runDeploymentRoot="_Mac_2026-01-20_05_32_45" />
|
||||||
|
</TestSettings>
|
||||||
|
<Results>
|
||||||
|
<UnitTestResult executionId="e21b7380-9510-4f6c-b33f-b8ccae8ed977" testId="d391ec73-c151-00a2-218d-506bf980c06b" testName="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenIntervalNotExceeded_ReturnsFalse" computerName="Mac" duration="00:00:00.0017257" startTime="2026-01-20T05:32:45.9105340+01:00" endTime="2026-01-20T05:32:45.9105410+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="e21b7380-9510-4f6c-b33f-b8ccae8ed977" />
|
||||||
|
<UnitTestResult executionId="6187b7d7-7b65-4f82-8125-bb75724b1d8b" testId="f87ac951-f9bb-5bc1-a589-4f242697f2f7" testName="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenEmailAlreadySent_ReturnsFalse" computerName="Mac" duration="00:00:00.0000283" startTime="2026-01-20T05:32:45.9116470+01:00" endTime="2026-01-20T05:32:45.9116470+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="6187b7d7-7b65-4f82-8125-bb75724b1d8b" />
|
||||||
|
<UnitTestResult executionId="993fdb22-2e6d-4e99-9daf-b604cc09eb49" testId="79b5babd-1f8c-4a15-303e-4439267af85a" testName="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldSendAlert_WhenIntervalExceededAndNotSent_ReturnsTrue" computerName="Mac" duration="00:00:00.0000677" startTime="2026-01-20T05:32:45.9115300+01:00" endTime="2026-01-20T05:32:45.9115300+01:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="993fdb22-2e6d-4e99-9daf-b604cc09eb49" />
|
||||||
|
</Results>
|
||||||
|
<TestDefinitions>
|
||||||
|
<UnitTest name="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldSendAlert_WhenIntervalExceededAndNotSent_ReturnsTrue" storage="/users/piotrkus/riderprojects/fa/heartbeatclient.tests/bin/debug/net8.0/heartbeatclient.tests.dll" id="79b5babd-1f8c-4a15-303e-4439267af85a">
|
||||||
|
<Execution id="993fdb22-2e6d-4e99-9daf-b604cc09eb49" />
|
||||||
|
<TestMethod codeBase="/Users/piotrkus/RiderProjects/FA/HeartBeatClient.Tests/bin/Debug/net8.0/HeartBeatClient.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="HeartBeatClient.Tests.HeartBeatDecisionServiceTests" name="ShouldSendAlert_WhenIntervalExceededAndNotSent_ReturnsTrue" />
|
||||||
|
</UnitTest>
|
||||||
|
<UnitTest name="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenEmailAlreadySent_ReturnsFalse" storage="/users/piotrkus/riderprojects/fa/heartbeatclient.tests/bin/debug/net8.0/heartbeatclient.tests.dll" id="f87ac951-f9bb-5bc1-a589-4f242697f2f7">
|
||||||
|
<Execution id="6187b7d7-7b65-4f82-8125-bb75724b1d8b" />
|
||||||
|
<TestMethod codeBase="/Users/piotrkus/RiderProjects/FA/HeartBeatClient.Tests/bin/Debug/net8.0/HeartBeatClient.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="HeartBeatClient.Tests.HeartBeatDecisionServiceTests" name="ShouldNotSendAlert_WhenEmailAlreadySent_ReturnsFalse" />
|
||||||
|
</UnitTest>
|
||||||
|
<UnitTest name="HeartBeatClient.Tests.HeartBeatDecisionServiceTests.ShouldNotSendAlert_WhenIntervalNotExceeded_ReturnsFalse" storage="/users/piotrkus/riderprojects/fa/heartbeatclient.tests/bin/debug/net8.0/heartbeatclient.tests.dll" id="d391ec73-c151-00a2-218d-506bf980c06b">
|
||||||
|
<Execution id="e21b7380-9510-4f6c-b33f-b8ccae8ed977" />
|
||||||
|
<TestMethod codeBase="/Users/piotrkus/RiderProjects/FA/HeartBeatClient.Tests/bin/Debug/net8.0/HeartBeatClient.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="HeartBeatClient.Tests.HeartBeatDecisionServiceTests" name="ShouldNotSendAlert_WhenIntervalNotExceeded_ReturnsFalse" />
|
||||||
|
</UnitTest>
|
||||||
|
</TestDefinitions>
|
||||||
|
<TestEntries>
|
||||||
|
<TestEntry testId="d391ec73-c151-00a2-218d-506bf980c06b" executionId="e21b7380-9510-4f6c-b33f-b8ccae8ed977" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||||
|
<TestEntry testId="f87ac951-f9bb-5bc1-a589-4f242697f2f7" executionId="6187b7d7-7b65-4f82-8125-bb75724b1d8b" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||||
|
<TestEntry testId="79b5babd-1f8c-4a15-303e-4439267af85a" executionId="993fdb22-2e6d-4e99-9daf-b604cc09eb49" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||||
|
</TestEntries>
|
||||||
|
<TestLists>
|
||||||
|
<TestList name="Wyniki, których nie ma na liście" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
|
||||||
|
<TestList name="Wszystkie załadowane wyniki" id="19431567-8539-422a-85d7-44ee4e166bda" />
|
||||||
|
</TestLists>
|
||||||
|
<ResultSummary outcome="Completed">
|
||||||
|
<Counters total="3" executed="3" passed="3" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
|
||||||
|
<Output>
|
||||||
|
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.1+6b60a9e56a (64-bit .NET 8.0.12)
|
||||||
|
[xUnit.net 00:00:00.02] Discovering: HeartBeatClient.Tests
|
||||||
|
[xUnit.net 00:00:00.03] Discovered: HeartBeatClient.Tests
|
||||||
|
[xUnit.net 00:00:00.03] Starting: HeartBeatClient.Tests
|
||||||
|
[xUnit.net 00:00:00.05] Finished: HeartBeatClient.Tests
|
||||||
|
</StdOut>
|
||||||
|
</Output>
|
||||||
|
</ResultSummary>
|
||||||
|
</TestRun>
|
||||||
2
parameters.env
Normal file
2
parameters.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VAULT_TOKEN=hvs.CAESID7fTT61Cs0C37k4pEr98_GXonsWfIgavM0a0nEAb9-qGh4KHGh2cy5DUnNnNjB5Yklla3c3cVNrYmdab01GSzM
|
||||||
|
VAULT_URL=https://vault.modwad.pl
|
||||||
Reference in New Issue
Block a user