Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# ==============================================================================
# Agenix Playwright Grid - Local Development Environment Configuration
# ==============================================================================
# This file is used by the startup script (scripts/run-local-dev.sh) to run
# Hub, Dashboard, and 3 Workers locally while infrastructure runs in Docker.
#
# Prerequisites:
# docker compose up redis postgres prometheus grafana -d
#
# Start all services:
# bash scripts/run-local-dev.sh (macOS/Linux)
# .\scripts\run-local-dev.ps1 (Windows)
# ==============================================================================

# ==============================================================================
# INFRASTRUCTURE (Docker Compose services)
# ==============================================================================
PLAYWRIGHT_VERSION=1.54.2
POSTGRES_PASSWORD=postgres
POSTGRES_USER=postgres
POSTGRES_DB=playwrightgrid

# ==============================================================================
# HUB CONFIGURATION (runs on localhost:5100)
# ==============================================================================
# NOTE: Changed from port 5000 to 5100 to avoid conflict with macOS AirPlay Receiver.
# macOS Monterey+ uses port 5000 for AirPlay Receiver by default.
# The startup scripts will set ASPNETCORE_URLS=http://localhost:5100 for the Hub.
# ==============================================================================
REDIS_URL=localhost:6379
HUB_RUNNER_SECRET=runner-secret
HUB_NODE_SECRET=node-secret
HUB_RESULTS_BACKEND=postgres
HUB_RESULTS_POSTGRES=Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playwrightgrid
HUB_BOOTSTRAP_ENABLED=1
HUB_BOOTSTRAP_ADMIN_USER=admin
HUB_BOOTSTRAP_ADMIN_PASSWORD=agenix-admin
HUB_BOOTSTRAP_DEFAULT_PROJECT=admin_default
[email protected]

# Telemetry (optional, for Prometheus/Grafana integration)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_SERVICE_NAME=hub

# ==============================================================================
# DASHBOARD CONFIGURATION (runs on localhost:3001)
# ==============================================================================
# The startup scripts will set ASPNETCORE_URLS=http://localhost:3001 for the Dashboard.
HUB_SIGNALR=http://localhost:5100/ws
HUB_CLIENT_SECRET=runner-secret

# ==============================================================================
# WORKER SHARED CONFIGURATION
# ==============================================================================
HUB_URL=http://localhost:5100
NODE_SECRET=node-secret
NODE_NODE_SECRET=node-node-secret
PUBLIC_WS_SCHEME=ws
PUBLIC_WS_HOST=127.0.0.1

# Chromium arguments (applies to all Chromium workers)
CHROMIUM_ARGS=--disable-dev-shm-usage --no-sandbox --disable-setuid-sandbox --no-proxy-server --disable-ipv6 --disable-quic --disable-http2 --disable-features=UseDNSHttpsSvcb

# ==============================================================================
# WORKER 1 (Chromium, WebSocket port 5200, HTTP port 5210)
# ==============================================================================
# Note: These are overridden by the startup script for each worker instance
WORKER1_NODE_ID=worker1
WORKER1_PUBLIC_WS_PORT=5200
WORKER1_HTTP_PORT=5210
WORKER1_POOL_CONFIG=AppB:Chromium:UAT=3

# ==============================================================================
# WORKER 2 (Chromium, WebSocket port 5201, HTTP port 5211)
# ==============================================================================
WORKER2_NODE_ID=worker2
WORKER2_PUBLIC_WS_PORT=5201
WORKER2_HTTP_PORT=5211
WORKER2_POOL_CONFIG=AppB:Chromium:UAT=3

# ==============================================================================
# WORKER 3 (Firefox + WebKit, WebSocket port 5202, HTTP port 5212)
# ==============================================================================
WORKER3_NODE_ID=worker3
WORKER3_PUBLIC_WS_PORT=5202
WORKER3_HTTP_PORT=5212
WORKER3_POOL_CONFIG=AppB:Firefox:UAT=2,AppB:Webkit:UAT=2
WORKER3_FIREFOX_ARGS=--headless
WORKER3_FIREFOX_PREFS={"network.dns.disablePrefetch":true,"browser.cache.disk.enable":false}

# ==============================================================================
# LOGGING (applies to all services)
# ==============================================================================
LOG_LEVEL=Information
LOG_LEVEL_OVERRIDES=Microsoft.AspNetCore=Warning,System.Net.Http.HttpClient=Warning

# ==============================================================================
# OPTIONAL: Metrics and Tracing
# ==============================================================================
# Uncomment to enable OpenTelemetry metrics and tracing for workers
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# OTEL_SERVICE_NAME=worker1 (set per worker in startup script)

# ==============================================================================
# EMAIL/SMTP CONFIGURATION (for password reset emails)
# ==============================================================================
# For local development with Mailpit (no real SMTP needed):
# docker compose up mailpit -d
# View emails at http://localhost:8025
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_USERNAME=test@localhost
SMTP_PASSWORD=test
[email protected]
SMTP_FROM_NAME=Agenix Playwright Grid
SMTP_USE_SSL=false
DASHBOARD_URL=http://localhost:3001

# For production with Gmail (requires App Password):
# 1. Enable 2FA on your Google account
# 2. Generate App Password: https://myaccount.google.com/apppasswords
# 3. Use the 16-character password below
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# [email protected]
# SMTP_PASSWORD=your-app-specific-password
# [email protected]
# SMTP_FROM_NAME=Agenix Playwright Grid
# SMTP_USE_SSL=true
# DASHBOARD_URL=https://your-domain.com

# For production with SendGrid:
# SMTP_HOST=smtp.sendgrid.net
# SMTP_PORT=587
# SMTP_USERNAME=apikey
# SMTP_PASSWORD=your-sendgrid-api-key
# [email protected]
# SMTP_FROM_NAME=Agenix Playwright Grid
# SMTP_USE_SSL=true
# DASHBOARD_URL=https://your-domain.com
250 changes: 250 additions & 0 deletions Agenix.PlaywrightGrid.Domain.Tests/AdminApiSerializationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
#region License

// Copyright (c) 2025 Agenix
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using NUnit.Framework;

namespace Agenix.PlaywrightGrid.Domain.Tests;

public class AdminApiSerializationTests
{
private static readonly JsonSerializerOptions WebJson = new(JsonSerializerDefaults.Web)
{
// Explicitly mirror Web defaults: camelCase + ignore null
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

[Test]
public void User_Serializes_CamelCase_And_Omits_Nulls_And_RoundTrips()
{
var now = new DateTime(2025, 1, 2, 3, 4, 5, DateTimeKind.Utc);
var u = new User
{
Id = "user-1",
Username = "Jane Doe",
Email = "[email protected]",
AccountRole = AccountRole.Administrator,
Status = UserStatus.Active,
ProjectsCount = 3,
LastLoginUtc = now,
CreatedUtc = now,
UpdatedUtc = now,
CreatedBy = "seed",
UpdatedBy = "system"
};

var json = JsonSerializer.Serialize(u, WebJson);
TestContext.WriteLine(json);
Assert.That(json, Does.Contain("\"id\":\"user-1\""));
Assert.That(json, Does.Contain("\"username\":\"Jane Doe\""));
Assert.That(json, Does.Contain("\"email\":\"[email protected]\""));
Assert.That(json, Does.Contain("\"projectsCount\":3"));
// Enums are numbers by default with System.Text.Json
Assert.That(json, Does.Contain("\"accountRole\":1"));
Assert.That(json, Does.Contain("\"status\":0"));

var rt = JsonSerializer.Deserialize<User>(json, WebJson)!;
Assert.That(rt.Id, Is.EqualTo(u.Id));
Assert.That(rt.Username, Is.EqualTo(u.Username));
Assert.That(rt.Email, Is.EqualTo(u.Email));
Assert.That(rt.AccountRole, Is.EqualTo(u.AccountRole));
Assert.That(rt.Status, Is.EqualTo(u.Status));
Assert.That(rt.ProjectsCount, Is.EqualTo(u.ProjectsCount));
Assert.That(rt.LastLoginUtc, Is.EqualTo(u.LastLoginUtc));
}

[Test]
public void User_Omits_Null_Email_And_LastLogin()
{
var now = new DateTime(2025, 2, 3, 4, 5, 6, DateTimeKind.Utc);
var u = new User
{
Id = "user-2",
Username = "John Smith",
Email = null,
CreatedUtc = now,
UpdatedUtc = now
};

var json = JsonSerializer.Serialize(u, WebJson);
TestContext.WriteLine(json);
Assert.That(json, Does.Contain("\"id\":\"user-2\""));
Assert.That(json, Does.Contain("\"username\":\"John Smith\""));
Assert.That(json, Does.Not.Contain("email"));
Assert.That(json, Does.Not.Contain("lastLoginUtc"));
}

[Test]
public void Project_Serializes_CamelCase_And_Omits_Null_Owner_And_RoundTrips()
{
var now = new DateTime(2025, 3, 4, 5, 6, 7, DateTimeKind.Utc);
var p = new Project
{
Key = "proj_key",
Name = "Project Name",
OwnerUserId = null,
Status = ProjectStatus.Active,
MembersCount = 5,
RunsCount = 42,
LastActivityUtc = now,
CreatedUtc = now,
UpdatedUtc = now,
CreatedBy = "admin",
UpdatedBy = "admin"
};

var json = JsonSerializer.Serialize(p, WebJson);
TestContext.WriteLine(json);
Assert.That(json, Does.Contain("\"key\":\"proj_key\""));
Assert.That(json, Does.Contain("\"name\":\"Project Name\""));
Assert.That(json, Does.Contain("\"membersCount\":5"));
Assert.That(json, Does.Contain("\"runsCount\":42"));
Assert.That(json, Does.Not.Contain("ownerUserId"));
// Enums are numbers by default
Assert.That(json, Does.Contain("\"status\":0"));

var rt = JsonSerializer.Deserialize<Project>(json, WebJson)!;
Assert.That(rt.Key, Is.EqualTo(p.Key));
Assert.That(rt.Name, Is.EqualTo(p.Name));
Assert.That(rt.OwnerUserId, Is.Null);
Assert.That(rt.Status, Is.EqualTo(p.Status));
Assert.That(rt.MembersCount, Is.EqualTo(p.MembersCount));
Assert.That(rt.RunsCount, Is.EqualTo(p.RunsCount));
Assert.That(rt.LastActivityUtc, Is.EqualTo(p.LastActivityUtc));
}

[Test]
public void Membership_Serializes_And_RoundTrips()
{
var now = new DateTime(2025, 4, 5, 6, 7, 8, DateTimeKind.Utc);
var m = new Membership
{
UserId = "user-1",
ProjectKey = "proj-1",
Role = ProjectRole.Member,
CreatedUtc = now,
UpdatedUtc = now,
CreatedBy = "admin",
UpdatedBy = "admin"
};

var json = JsonSerializer.Serialize(m, WebJson);
TestContext.WriteLine(json);
Assert.That(json, Does.Contain("\"userId\":\"user-1\""));
Assert.That(json, Does.Contain("\"projectKey\":\"proj-1\""));
// Enums are numbers by default
Assert.That(json, Does.Contain("\"role\":1"));

var rt = JsonSerializer.Deserialize<Membership>(json, WebJson)!;
Assert.That(rt.UserId, Is.EqualTo(m.UserId));
Assert.That(rt.ProjectKey, Is.EqualTo(m.ProjectKey));
Assert.That(rt.Role, Is.EqualTo(m.Role));
}

[Test]
public void Launch_Serializes_CamelCase_And_Omits_Nulls_And_RoundTrips()
{
var startTime = new DateTimeOffset(2025, 1, 15, 10, 30, 0, TimeSpan.Zero);
var finishTime = new DateTimeOffset(2025, 1, 15, 10, 30, 3, TimeSpan.Zero);

var launch = new Launch
{
Id = Guid.Parse("12345678-1234-1234-1234-123456789012"),
Name = "Demo Api Tests",
Description = "Demonstration launch.",
Attributes = new[] { "tag1", "tag2", "platform:x64", "build:3.4.7.47.10", "demo", "platform:macos" },
OwnerApiKey = "test-api-key-123",
ProjectKey = "demo-project",
StartTime = startTime,
FinishTime = finishTime,
LaunchNumber = 5,
TotalTestRuns = 30,
FinishedTestRuns = 30,
RunningTestRuns = 0,
StoppedTestRuns = 0,
ErroredTestRuns = 0
};

var json = JsonSerializer.Serialize(launch, WebJson);
TestContext.WriteLine(json);

Assert.That(json, Does.Contain("\"id\":\"12345678-1234-1234-1234-123456789012\""));
Assert.That(json, Does.Contain("\"name\":\"Demo Api Tests\""));
Assert.That(json, Does.Contain("\"description\":\"Demonstration launch.\""));
Assert.That(json, Does.Contain("\"ownerApiKey\":\"test-api-key-123\""));
Assert.That(json, Does.Contain("\"projectKey\":\"demo-project\""));
Assert.That(json, Does.Contain("\"launchNumber\":5"));
Assert.That(json, Does.Contain("\"totalTestRuns\":30"));
Assert.That(json, Does.Contain("\"finishedTestRuns\":30"));
Assert.That(json, Does.Contain("\"durationSeconds\":3"));
Assert.That(json, Does.Contain("\"isRunning\":false"));
Assert.That(json, Does.Contain("\"attributes\":[\"tag1\",\"tag2\",\"platform:x64\""));

var rt = JsonSerializer.Deserialize<Launch>(json, WebJson)!;
Assert.That(rt.Id, Is.EqualTo(launch.Id));
Assert.That(rt.Name, Is.EqualTo(launch.Name));
Assert.That(rt.Description, Is.EqualTo(launch.Description));
Assert.That(rt.Attributes, Is.EqualTo(launch.Attributes));
Assert.That(rt.OwnerApiKey, Is.EqualTo(launch.OwnerApiKey));
Assert.That(rt.ProjectKey, Is.EqualTo(launch.ProjectKey));
Assert.That(rt.StartTime, Is.EqualTo(launch.StartTime));
Assert.That(rt.FinishTime, Is.EqualTo(launch.FinishTime));
Assert.That(rt.LaunchNumber, Is.EqualTo(launch.LaunchNumber));
Assert.That(rt.TotalTestRuns, Is.EqualTo(launch.TotalTestRuns));
Assert.That(rt.DurationSeconds, Is.EqualTo(3.0));
Assert.That(rt.IsRunning, Is.False);
}

[Test]
public void Launch_Omits_Null_Description_And_FinishTime_For_Running_Launch()
{
var startTime = new DateTimeOffset(2025, 1, 15, 10, 30, 0, TimeSpan.Zero);

var launch = new Launch
{
Id = Guid.NewGuid(),
Name = "Running Tests",
Description = null,
Attributes = new[] { "platform:linux" },
OwnerApiKey = "api-key-456",
ProjectKey = "test-proj",
StartTime = startTime,
FinishTime = null,
LaunchNumber = 1,
TotalTestRuns = 10,
FinishedTestRuns = 5,
RunningTestRuns = 5,
StoppedTestRuns = 0,
ErroredTestRuns = 0
};

var json = JsonSerializer.Serialize(launch, WebJson);
TestContext.WriteLine(json);

Assert.That(json, Does.Contain("\"name\":\"Running Tests\""));
Assert.That(json, Does.Not.Contain("\"description\""));
Assert.That(json, Does.Not.Contain("\"finishTime\""));
Assert.That(json, Does.Not.Contain("\"durationSeconds\""));
Assert.That(json, Does.Contain("\"isRunning\":true"));
Assert.That(json, Does.Contain("\"runningTestRuns\":5"));
}
}
Loading
Loading