Skip to content

Tests (Refactored) Docker #13

Tests (Refactored) Docker

Tests (Refactored) Docker #13

name: Tests (Refactored) Docker
on:
workflow_dispatch:
inputs:
platformDockerTag:
description: "Platform Docker image tag"
required: false
type: string
default: "linux-latest"
frontendZipUrl:
description: "Frontend zip URL ('latest' for latest vc-frontend release)"
required: false
type: string
default: "latest"
jobs:
tests:
timeout-minutes: 60
runs-on: ubuntu-22.04
permissions:
contents: read
checks: write
pull-requests: write
issues: read
env:
GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }}
FORCE_COLOR: "1"
steps:
# ──────────────────────────────────────────────
# 1. Checkout & install modules
# ──────────────────────────────────────────────
- name: Checkout testing repo
uses: actions/checkout@v4
with:
repository: VirtoCommerce/vc-testing-module
ref: ${{ github.ref_name }}
path: vc-testing-module
token: ${{ secrets.REPO_TOKEN }}
- name: Install VirtoCommerce.GlobalTool
uses: VirtoCommerce/vc-github-actions/setup-vcbuild@master
- name: Install modules and pull platform image
run: |
mkdir modules && cd modules
vc-build install --package-manifest-path $GITHUB_WORKSPACE/vc-testing-module/backend-packages.json --skip-dependency-solving
docker pull ghcr.io/virtocommerce/platform:${{ inputs.platformDockerTag }}
docker tag ghcr.io/virtocommerce/platform:${{ inputs.platformDockerTag }} platform:local-latest
# ──────────────────────────────────────────────
# 2. Build frontend Docker image
# ──────────────────────────────────────────────
- name: Build frontend Docker image
shell: pwsh
run: |
if ("${{ inputs.frontendZipUrl }}" -eq "latest") {
$releaseInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/VirtoCommerce/vc-frontend/releases/latest"
$zipUrl = $releaseInfo.assets.browser_download_url
Write-Host "Using latest frontend release: $zipUrl"
} else {
$zipUrl = "${{ inputs.frontendZipUrl }}"
Write-Host "Using provided frontend zip: $zipUrl"
}
$fileName = Split-Path $zipUrl -Leaf
Invoke-WebRequest -Uri $zipUrl -OutFile $fileName -UseBasicParsing -OperationTimeoutSeconds 15 -RetryIntervalSec 1 -MaximumRetryCount 3
mkdir -p frontend
unzip -o $fileName -d ./frontend
@'
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
add_header 'Access-Control-Allow-Origin' '*';
}
location ~* \.(json|png|ico|gif|jpg|jpeg|css|js|xml|txt)$ {
try_files $uri /assets/stores/B2B-store$uri /Themes/B2B-store/default$uri =404;
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, must-revalidate, proxy-revalidate";
error_page 404 = @static_404;
}
location @static_404 {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
return 404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html { root /usr/share/nginx/html; }
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
location /files { proxy_pass http://vc-platform-web; }
location /connect/token { proxy_pass http://vc-platform-web; }
location /graphql { proxy_pass http://vc-platform-web; }
location /revoke/token { proxy_pass http://vc-platform-web; }
location /api/files { proxy_pass http://vc-platform-web; }
location /cms-content { proxy_pass http://vc-platform-web; }
location /hub/ {
proxy_pass http://vc-platform-web;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
'@ | Set-Content -Path nginx.conf
@'
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY ./frontend/default/ /usr/share/nginx/html
EXPOSE 80
'@ | Set-Content -Path Dockerfile.frontend
docker build -f Dockerfile.frontend -t nginx_frontend:local-latest .
# ──────────────────────────────────────────────
# 3. Start Docker infrastructure
# ──────────────────────────────────────────────
- name: Create docker-compose.yml
run: |
cat > docker-compose.yml << 'COMPOSE'
services:
vc-db:
image: mcr.microsoft.com/mssql/server:2017-latest
ports:
- "1433:1433"
environment:
- ACCEPT_EULA=Y
- MSSQL_PID=Express
- SA_PASSWORD=v!rto_Labs!
networks:
- virto
es:
image: docker.elastic.co/elasticsearch/elasticsearch:8.18.0
volumes:
- esdata01:/usr/share/elasticsearch/data
ports:
- "9200:9200"
networks:
- virto
environment:
- node.name=es
- cluster.name=elasticsearch
- cluster.initial_master_nodes=es
- ELASTIC_PASSWORD=v!rto_Labs!
- bootstrap.memory_lock=true
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
- xpack.license.self_generated.type=basic
- xpack.ml.use_auto_machine_memory_percent=true
mem_limit: 1g
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:9200"]
interval: 10s
timeout: 10s
retries: 120
nginx_frontend:
depends_on:
- vc-platform-web
image: nginx_frontend:local-latest
ports:
- "80:80"
networks:
- virto
vc-platform-web:
image: platform:local-latest
ports:
- "8090:80"
environment:
- ASPNETCORE_URLS=http://+
- VirtoCommerce__AllowInsecureHttp=true
- VirtoCommerce__Hangfire__JobStorageType=Memory
- ConnectionStrings__VirtoCommerce=Data Source=vc-db;Initial Catalog=VirtoCommerce3docker;Persist Security Info=True;User ID=sa;Password=v!rto_Labs!;MultipleActiveResultSets=False;Connect Timeout=360;TrustServerCertificate=True;
- Assets__FileSystem__PublicUrl=http://localhost:8090/assets/
- Content__FileSystem__PublicUrl=http://localhost:8090/cms-content/
- Search__Provider=ElasticSearch8
- Search__ElasticSearch8__Server=http://es:9200
- Search__ElasticSearch8__User=elastic
- Search__ElasticSearch8__Key=v!rto_Labs!
- Search__ElasticSearch8__EnableCompatibilityMode=true
- Search__PickupLocationFullTextSearchEnabled=true
- IdentityOptions__User__MaxPasswordAge=0
- Notifications__DefaultSender=virto.start@virtoway.com
- Notifications__Gateway=SendGrid
- Notifications__SendGrid__ApiKey=${SENDGRID_API_KEY}
depends_on:
- vc-db
- es
entrypoint: ["/wait-for-it.sh", "vc-db:1433", "-t", "120", "--", "dotnet", "VirtoCommerce.Platform.Web.dll"]
volumes:
- ./modules/modules:/opt/virtocommerce/platform/modules
- ./modules/app_data:/opt/virtocommerce/platform/app_data
networks:
- virto
restart: unless-stopped
volumes:
esdata01:
driver: local
networks:
virto:
COMPOSE
- name: Start database and Elasticsearch
env:
SENDGRID_API_KEY: ${{ secrets.SENDGRID_APIKEY_4E2E_AUTOTESTS }}
run: |
docker compose --project-name virtocommerce up -d vc-db es
sleep 15
- name: Start platform and frontend
env:
SENDGRID_API_KEY: ${{ secrets.SENDGRID_APIKEY_4E2E_AUTOTESTS }}
run: |
docker compose --project-name virtocommerce up -d
echo "Waiting for platform to start..."
for i in $(seq 1 20); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8090/connect/token 2>/dev/null) || true
echo "Attempt $i/20: HTTP $HTTP_CODE"
if [ "$HTTP_CODE" -ge 200 ] 2>/dev/null && [ "$HTTP_CODE" -lt 500 ] 2>/dev/null; then
echo "Platform is ready!"
break
fi
sleep 15
done
echo "Checking frontend..."
for i in $(seq 1 10); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost 2>/dev/null) || true
if [ "$HTTP_CODE" = "200" ]; then
echo "Frontend is ready!"
break
fi
sleep 5
done
# ──────────────────────────────────────────────
# 4. Prepare platform
# ──────────────────────────────────────────────
- name: Reset admin password
shell: pwsh
env:
SECRET_ENV_FILE: ${{ secrets.VC_TESTING_MODULE_ENV_FILE_NEW }}
run: |
if ($env:SECRET_ENV_FILE -match 'ADMIN_PASSWORD=(\S+)') {
$newAdminPassword = $matches[1]
} else {
$newAdminPassword = 'store'
}
$body = "grant_type=password&username=admin&password=store"
$headers = @{ "Content-Type" = "application/x-www-form-urlencoded" }
$token = ((Invoke-WebRequest -Uri "http://localhost:8090/connect/token" -Body $body -Headers $headers -Method POST).Content | ConvertFrom-Json).access_token
$authHeaders = @{ "Content-Type" = "application/json-patch+json"; "Authorization" = "Bearer $token" }
$resetBody = @{ "newPassword" = $newAdminPassword; "forcePasswordChangeOnNextSignIn" = $false } | ConvertTo-Json
Invoke-WebRequest -Uri "http://localhost:8090/api/platform/security/users/admin/resetpassword" -Body $resetBody -Headers $authHeaders -Method POST
Write-Host "Admin password reset successfully"
# ──────────────────────────────────────────────
# 5. Set up Python & dependencies
# ──────────────────────────────────────────────
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Create .env and install dependencies
working-directory: vc-testing-module/_refactored
env:
SECRET_ENV_FILE: ${{ secrets.VC_TESTING_MODULE_ENV_FILE_NEW }}
run: |
echo "$SECRET_ENV_FILE" > .env
chmod 600 .env
sed -i 's|^BACKEND_BASE_URL=.*|BACKEND_BASE_URL=http://localhost:8090|g' .env
sed -i 's|^FRONTEND_BASE_URL=.*|FRONTEND_BASE_URL=http://localhost|g' .env
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -e .
playwright install --with-deps chromium
# ──────────────────────────────────────────────
# 6. Seed data & trigger indexing
# ──────────────────────────────────────────────
- name: Seed test data
working-directory: vc-testing-module/_refactored
run: |
source .venv/bin/activate
python -m dataset.dataset_manager --seed --mode ci
- name: Trigger search indexing
shell: pwsh
env:
SECRET_ENV_FILE: ${{ secrets.VC_TESTING_MODULE_ENV_FILE_NEW }}
run: |
$platformUrl = "http://localhost:8090"
if ($env:SECRET_ENV_FILE -match 'ADMIN_PASSWORD=(\S+)') { $adminPassword = $matches[1] } else { $adminPassword = 'store' }
$headers = @{ "Content-Type" = "application/x-www-form-urlencoded" }
$token = ((Invoke-WebRequest -Uri "$platformUrl/connect/token" -Body "grant_type=password&username=admin&password=$adminPassword" -Headers $headers -Method POST).Content | ConvertFrom-Json).access_token
$authHeaders = @{ "Content-Type" = "application/json-patch+json"; "Authorization" = "Bearer $token" }
$indexBody = @(
@{ "DocumentType" = "Member"; "DeleteExistingIndex" = $true },
@{ "DocumentType" = "Product"; "DeleteExistingIndex" = $true },
@{ "DocumentType" = "Category"; "DeleteExistingIndex" = $true },
@{ "DocumentType" = "CustomerOrder"; "DeleteExistingIndex" = $true },
@{ "DocumentType" = "PickupLocation"; "DeleteExistingIndex" = $true }
)
$result = Invoke-WebRequest -Uri "$platformUrl/api/search/indexes/index" -Body ($indexBody | ConvertTo-Json) -Headers $authHeaders -Method POST | ConvertFrom-Json
do {
$job = Invoke-WebRequest -Uri "$platformUrl/api/platform/jobs/$($result.JobId)" -Headers $authHeaders -Method GET | ConvertFrom-Json
Write-Host "Indexing completed: $($job.completed)"
if (-not $job.completed) { Start-Sleep -Seconds 5 }
} while (-not $job.completed)
Write-Host "Indexing finished."
# ──────────────────────────────────────────────
# 7. Run tests
# ──────────────────────────────────────────────
- name: Clean allure-results
working-directory: vc-testing-module/_refactored
run: rm -rf allure-results
- name: Run GraphQL tests
working-directory: vc-testing-module/_refactored
run: |
source .venv/bin/activate
pytest tests/graphql -m "not ignore" -v -s --color=yes \
--junitxml=graphql-junit.xml
- name: Run REST API tests (parallel-safe)
if: success() || failure()
working-directory: vc-testing-module/_refactored
run: |
source .venv/bin/activate
pytest tests/restapi -m "restapi and not ignore and not serial" -v -s --color=yes \
--junitxml=restapi-junit.xml
- name: Run E2E tests
if: success() || failure()
working-directory: vc-testing-module/_refactored
run: |
source .venv/bin/activate
pytest tests/e2e -m "not ignore" --browser chromium -v -s --color=yes \
--junitxml=e2e-junit.xml
- name: Run REST API tests (serial — global state mutators)
if: success() || failure()
working-directory: vc-testing-module/_refactored
run: |
source .venv/bin/activate
pytest tests/restapi -m "restapi and serial and not ignore" -v -s --color=yes \
--junitxml=restapi-serial-junit.xml \
--deselect tests/restapi/platform/test_misc.py::test_restart_platform
- name: Run REST API test — restart platform (last)
if: success() || failure()
working-directory: vc-testing-module/_refactored
run: |
source .venv/bin/activate
pytest tests/restapi/platform/test_misc.py::test_restart_platform -v -s --color=yes \
--junitxml=restapi-restart-junit.xml
# ──────────────────────────────────────────────
# 8. Collect results
# ──────────────────────────────────────────────
- name: Install Allure CLI
if: always()
run: |
ALLURE_VERSION=2.29.0
wget -q -O allure.tgz \
"https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/allure-${ALLURE_VERSION}.tgz"
sudo tar -xzf allure.tgz -C /opt
sudo ln -sf "/opt/allure-${ALLURE_VERSION}/bin/allure" /usr/local/bin/allure
allure --version
- name: Generate Allure HTML report
if: always()
working-directory: vc-testing-module/_refactored
run: |
if [ -d allure-results ] && [ -n "$(ls -A allure-results 2>/dev/null)" ]; then
allure generate allure-results --clean -o allure-report
else
echo "::warning::allure-results is empty — skipping report render"
fi
- name: Bundle Allure into single HTML
if: always()
working-directory: vc-testing-module/_refactored
run: |
if [ -d allure-report ]; then
pip install allure-combine
allure-combine allure-report
ls -la allure-report/complete.html
else
echo "::warning::allure-report missing — skipping allure-combine"
fi
- name: Stage consolidated report
if: always()
working-directory: vc-testing-module/_refactored
run: |
mkdir -p report
[ -d allure-report ] && mv allure-report report/ || echo "no allure-report"
[ -d har-output ] && mv har-output report/ || echo "no har-output"
[ -d test-results ] && mv test-results report/ || echo "no test-results"
[ -d screenshots ] && mv screenshots report/ || echo "no screenshots"
[ -f graphql-junit.xml ] && mv graphql-junit.xml report/ || echo "no graphql-junit.xml"
[ -f restapi-junit.xml ] && mv restapi-junit.xml report/ || echo "no restapi-junit.xml"
[ -f restapi-serial-junit.xml ] && mv restapi-serial-junit.xml report/ || echo "no restapi-serial-junit.xml"
[ -f restapi-restart-junit.xml ] && mv restapi-restart-junit.xml report/ || echo "no restapi-restart-junit.xml"
[ -f e2e-junit.xml ] && mv e2e-junit.xml report/ || echo "no e2e-junit.xml"
cat > report/index.html << 'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=allure-report/complete.html">
<title>Refactored Test Results</title>
</head>
<body>
<p>Redirecting to the <a href="allure-report/complete.html">Allure report</a>.
If nothing happens, open <code>allure-report/complete.html</code> manually.</p>
</body>
</html>
HTML
cat > report/README.md << 'EOF'
# Refactored Test Results
Run: ${{ github.run_number }} (attempt ${{ github.run_attempt }})
Commit: ${{ github.sha }}
Branch: ${{ github.ref_name }}
## What's in this bundle
| Path | Use it for |
|---|---|
| `index.html` | Landing page — double-click to open the standalone Allure report. |
| `allure-report/complete.html` | Standalone single-file Allure report (GraphQL + REST API + E2E combined). Works from `file://` — no server needed. |
| `allure-report/` (the rest) | Original multi-file Allure report. Needs `python -m http.server` or `allure open allure-report/`. |
| `har-output/<module>/<test>.har` | Per-test HTTP traces (HAR 1.2) from REST API tests, grouped by module. Import into Chrome DevTools Network tab. Auth headers redacted. |
| `test-results/` | Playwright trace zips per failed E2E test. Open with `playwright show-trace <file>.zip` or at [trace.playwright.dev](https://trace.playwright.dev). |
| `screenshots/` | Full-page screenshots captured on E2E test failure. Attached to Allure report automatically. |
| `graphql-junit.xml` | JUnit XML for GraphQL tests. |
| `restapi-junit.xml` | JUnit XML for REST API tests. |
| `e2e-junit.xml` | JUnit XML for E2E tests. |
The same JUnit results drive the per-test PR comment and the per-file breakdown on the run's Summary tab.
EOF
- name: Upload consolidated test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ github.run_number }}-${{ github.run_attempt }}
path: vc-testing-module/_refactored/report/
if-no-files-found: warn
retention-days: 30
- name: Publish test results summary
if: always()
uses: EnricoMi/publish-unit-test-result-action@v2
with:
files: |
vc-testing-module/_refactored/report/graphql-junit.xml
vc-testing-module/_refactored/report/restapi-junit.xml
vc-testing-module/_refactored/report/restapi-serial-junit.xml
vc-testing-module/_refactored/report/restapi-restart-junit.xml
vc-testing-module/_refactored/report/e2e-junit.xml
check_name: Test Results (Refactored)
comment_mode: always
report_individual_runs: true
- name: Per-file test summary
if: always()
working-directory: vc-testing-module/_refactored
run: |
python <<'PYEOF'
import os
import xml.etree.ElementTree as ET
from collections import defaultdict
from pathlib import Path
stats = defaultdict(
lambda: {"total": 0, "passed": 0, "failed": 0, "errored": 0, "skipped": 0, "time": 0.0}
)
for xml_name in ["report/graphql-junit.xml", "report/restapi-junit.xml", "report/restapi-serial-junit.xml", "report/restapi-restart-junit.xml", "report/e2e-junit.xml"]:
xml_path = Path(xml_name)
if not xml_path.exists():
continue
tree = ET.parse(xml_path)
suite_label = "graphql" if "graphql" in xml_name else ("restapi" if "restapi" in xml_name else "e2e")
for tc in tree.iter("testcase"):
classname = tc.attrib.get("classname", "")
bucket = classname.rsplit(".", 1)[-1] if classname else "(unknown)"
key = f"{suite_label}/{bucket}"
s = stats[key]
s["total"] += 1
try:
s["time"] += float(tc.attrib.get("time") or 0)
except ValueError:
pass
if tc.find("failure") is not None:
s["failed"] += 1
elif tc.find("error") is not None:
s["errored"] += 1
elif tc.find("skipped") is not None:
s["skipped"] += 1
else:
s["passed"] += 1
if not stats:
print("No JUnit XML found — skipping summary")
raise SystemExit(0)
lines = [
"## Refactored tests — per-file breakdown",
"",
"| Suite / Test file | Total | Passed | Failed | Errored | Skipped | Time |",
"|---|---:|---:|---:|---:|---:|---:|",
]
totals = {"total": 0, "passed": 0, "failed": 0, "errored": 0, "skipped": 0, "time": 0.0}
for key in sorted(stats):
s = stats[key]
for k in totals:
totals[k] += s[k]
lines.append(
f"| `{key}` | {s['total']} | {s['passed']} | {s['failed']} | "
f"{s['errored']} | {s['skipped']} | {s['time']:.1f}s |"
)
lines.append(
f"| **Total** | **{totals['total']}** | **{totals['passed']}** | "
f"**{totals['failed']}** | **{totals['errored']}** | "
f"**{totals['skipped']}** | **{totals['time']:.1f}s** |"
)
lines.append("")
output = "\n".join(lines)
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if summary_path:
with open(summary_path, "a", encoding="utf-8") as f:
f.write(output + "\n")
print(output)
PYEOF
# ──────────────────────────────────────────────
# 9. Diagnostics on failure
# ──────────────────────────────────────────────
- name: Print container logs on failure
if: failure()
run: |
echo "=== Platform logs ==="
docker logs virtocommerce-vc-platform-web-1 --tail 100
echo "=== Elasticsearch logs ==="
docker logs virtocommerce-es-1 --tail 50