mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 00:26:07 +02:00
feat: Add initial implementation of CI/CD workflows, documentation, and testing framework
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"docfx": {
|
||||||
|
"version": "2.78.3",
|
||||||
|
"commands": [
|
||||||
|
"docfx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
name: CI / Release / Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-markdown:
|
||||||
|
name: Markdown Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run markdownlint-cli2
|
||||||
|
uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
|
with:
|
||||||
|
globs: |
|
||||||
|
**/*.md
|
||||||
|
!.dev/**
|
||||||
|
!docs/api/**
|
||||||
|
!**/node_modules/**
|
||||||
|
!**/bin/**
|
||||||
|
!**/obj/**
|
||||||
|
|
||||||
|
detect-changes:
|
||||||
|
name: Detect Changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
src_changed: ${{ steps.check.outputs.src_changed }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Check for src/ changes
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/' | wc -l)
|
||||||
|
if [ "$CHANGED" -gt 0 ]; then
|
||||||
|
echo "src_changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "src_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Build & Test
|
||||||
|
needs: [lint-markdown, detect-changes]
|
||||||
|
if: needs.detect-changes.outputs.src_changed == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore src/EchoHub.slnx
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build src/EchoHub.slnx --no-restore --configuration Release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test src/EchoHub.slnx --no-build --configuration Release --verbosity normal
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
needs: [lint-markdown, detect-changes, test]
|
||||||
|
if: needs.detect-changes.outputs.src_changed == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Read version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check if release exists
|
||||||
|
id: check_release
|
||||||
|
run: |
|
||||||
|
if gh release view "${{ steps.version.outputs.tag }}" &>/dev/null; then
|
||||||
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish Server win-x64
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r win-x64 --self-contained true -o publish/server-win-x64
|
||||||
|
|
||||||
|
- name: Publish Server linux-x64
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r linux-x64 --self-contained true -o publish/server-linux-x64
|
||||||
|
|
||||||
|
- name: Publish Server osx-x64
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r osx-x64 --self-contained true -o publish/server-osx-x64
|
||||||
|
|
||||||
|
- name: Publish Client win-x64
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -o publish/client-win-x64
|
||||||
|
|
||||||
|
- name: Publish Client linux-x64
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-x64 --self-contained true -o publish/client-linux-x64
|
||||||
|
|
||||||
|
- name: Publish Client osx-x64
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-x64 --self-contained true -o publish/client-osx-x64
|
||||||
|
|
||||||
|
- name: Zip artifacts
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: |
|
||||||
|
cd publish
|
||||||
|
zip -r ../EchoHub-Server-win-x64.zip server-win-x64/
|
||||||
|
zip -r ../EchoHub-Server-linux-x64.zip server-linux-x64/
|
||||||
|
zip -r ../EchoHub-Server-osx-x64.zip server-osx-x64/
|
||||||
|
zip -r ../EchoHub-Client-win-x64.zip client-win-x64/
|
||||||
|
zip -r ../EchoHub-Client-linux-x64.zip client-linux-x64/
|
||||||
|
zip -r ../EchoHub-Client-osx-x64.zip client-osx-x64/
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
run: |
|
||||||
|
gh release create "${{ steps.version.outputs.tag }}" \
|
||||||
|
--title "EchoHub ${{ steps.version.outputs.tag }}" \
|
||||||
|
--generate-notes \
|
||||||
|
EchoHub-Server-win-x64.zip \
|
||||||
|
EchoHub-Server-linux-x64.zip \
|
||||||
|
EchoHub-Server-osx-x64.zip \
|
||||||
|
EchoHub-Client-win-x64.zip \
|
||||||
|
EchoHub-Client-linux-x64.zip \
|
||||||
|
EchoHub-Client-osx-x64.zip
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
docs:
|
||||||
|
name: Build & Deploy Docs
|
||||||
|
needs: lint-markdown
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Restore .NET tools
|
||||||
|
run: dotnet tool restore
|
||||||
|
|
||||||
|
- name: Build solution
|
||||||
|
run: dotnet build src/EchoHub.slnx --configuration Release
|
||||||
|
|
||||||
|
- name: Generate documentation
|
||||||
|
run: dotnet docfx docs/docfx.json
|
||||||
|
|
||||||
|
- name: Deploy to web branch
|
||||||
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./docs/_site
|
||||||
|
publish_branch: web
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: PR Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-markdown:
|
||||||
|
name: Markdown Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run markdownlint-cli2
|
||||||
|
uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
|
with:
|
||||||
|
globs: |
|
||||||
|
**/*.md
|
||||||
|
!.dev/**
|
||||||
|
!docs/api/**
|
||||||
|
!**/node_modules/**
|
||||||
|
!**/bin/**
|
||||||
|
!**/obj/**
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Build & Test
|
||||||
|
needs: lint-markdown
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check for src/ changes
|
||||||
|
id: changes
|
||||||
|
run: |
|
||||||
|
git fetch origin ${{ github.base_ref }} --depth=1
|
||||||
|
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'src/' | wc -l)
|
||||||
|
if [ "$CHANGED" -gt 0 ]; then
|
||||||
|
echo "src_changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "src_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
if: steps.changes.outputs.src_changed == 'true'
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
if: steps.changes.outputs.src_changed == 'true'
|
||||||
|
run: dotnet restore src/EchoHub.slnx
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: steps.changes.outputs.src_changed == 'true'
|
||||||
|
run: dotnet build src/EchoHub.slnx --no-restore --configuration Release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
if: steps.changes.outputs.src_changed == 'true'
|
||||||
|
run: dotnet test src/EchoHub.slnx --no-build --configuration Release --verbosity normal
|
||||||
|
|
||||||
|
- name: Skip notice
|
||||||
|
if: steps.changes.outputs.src_changed == 'false'
|
||||||
|
run: echo "No changes in src/ — skipping build and test"
|
||||||
@@ -429,3 +429,8 @@ CLAUDE.md
|
|||||||
*.db-*
|
*.db-*
|
||||||
src/EchoHub.Server/uploads/*
|
src/EchoHub.Server/uploads/*
|
||||||
.claude/*
|
.claude/*
|
||||||
|
|
||||||
|
# DocFx generated output
|
||||||
|
docs/_site/
|
||||||
|
docs/api/
|
||||||
|
!docs/api/.gitkeep
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"MD013": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD041": false
|
||||||
|
},
|
||||||
|
"ignores": [
|
||||||
|
".dev/**",
|
||||||
|
"docs/api/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/bin/**",
|
||||||
|
"**/obj/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -76,7 +76,7 @@ graph TD
|
|||||||
### Client
|
### Client
|
||||||
|
|
||||||
- **Runs in your terminal** — no browser, no Electron, no 500MB of bundled Chromium
|
- **Runs in your terminal** — no browser, no Electron, no 500MB of bundled Chromium
|
||||||
- **6 built-in themes** — including `hacker` for when you want to feel like you're in a movie
|
- **13 built-in themes** — including `hacker` for when you want to feel like you're in a movie
|
||||||
- **Slash commands** — `/join`, `/send`, `/status`, `/theme`, etc.
|
- **Slash commands** — `/join`, `/send`, `/status`, `/theme`, etc.
|
||||||
- **Colored nicknames** — pick your hex color, express yourself
|
- **Colored nicknames** — pick your hex color, express yourself
|
||||||
- **File/image sharing** — local files or URLs
|
- **File/image sharing** — local files or URLs
|
||||||
@@ -151,6 +151,13 @@ dotnet build src/EchoHub.slnx
|
|||||||
| `light` | Black on white — for the brave |
|
| `light` | Black on white — for the brave |
|
||||||
| `hacker` | Green on black — *I'm in* |
|
| `hacker` | Green on black — *I'm in* |
|
||||||
| `solarized` | Cyan/yellow on dark gray — for the refined |
|
| `solarized` | Cyan/yellow on dark gray — for the refined |
|
||||||
|
| `dracula` | Purple accents on black — the classic dark theme |
|
||||||
|
| `monokai` | Yellow highlights on black — warm and familiar |
|
||||||
|
| `nord` | Cool blues — arctic vibes |
|
||||||
|
| `gruvbox` | Earthy yellows on black — retro warmth |
|
||||||
|
| `ocean` | Cyan on deep blue — underwater aesthetics |
|
||||||
|
| `highcontrast` | Bright yellow on black — maximum readability |
|
||||||
|
| `rosepine` | Muted pinks on black — cozy and soft |
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
EchoHub follows a decentralized model where each server is fully independent. There is no central authority or account federation. Users create one account per server.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### EchoHub.Core
|
||||||
|
|
||||||
|
Shared library containing:
|
||||||
|
|
||||||
|
- **Models**: `User`, `Channel`, `Message`, `RefreshToken`
|
||||||
|
- **DTOs**: Record types for API requests/responses
|
||||||
|
- **Contracts**: `IEchoHubClient` -- the strongly-typed SignalR client interface
|
||||||
|
- **Constants**: `ValidationConstants` (shared regex patterns), `HubConstants`
|
||||||
|
|
||||||
|
### EchoHub.Server
|
||||||
|
|
||||||
|
ASP.NET Core web application:
|
||||||
|
|
||||||
|
- **Controllers**: REST API endpoints for auth, channels, users, files, server info
|
||||||
|
- **Hubs**: SignalR `ChatHub` for real-time messaging
|
||||||
|
- **Auth**: JWT token service (15-min access tokens, 30-day refresh tokens)
|
||||||
|
- **Data**: EF Core with SQLite
|
||||||
|
- **Services**: Presence tracking, file storage, image-to-ASCII conversion
|
||||||
|
|
||||||
|
### EchoHub.Client
|
||||||
|
|
||||||
|
Terminal.Gui v2 TUI application:
|
||||||
|
|
||||||
|
- **UI**: Main window, dialogs, chat renderer with ANSI color support
|
||||||
|
- **Services**: API client with automatic token refresh, SignalR connection wrapper
|
||||||
|
- **Themes**: 6 built-in color themes
|
||||||
|
- **Config**: Client configuration management
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
- REST API for authentication, profile management, channel CRUD, file uploads
|
||||||
|
- SignalR WebSocket for real-time messaging and presence updates
|
||||||
|
- JWT tokens passed via query string for SignalR authentication
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
||||||
|
|
||||||
|
## Run the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project src/EchoHub.Server
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run, the server automatically:
|
||||||
|
|
||||||
|
1. Creates `appsettings.json` from the example config
|
||||||
|
2. Generates a secure JWT secret
|
||||||
|
3. Creates the SQLite database with a `#general` channel
|
||||||
|
|
||||||
|
## Run the Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project src/EchoHub.Client
|
||||||
|
```
|
||||||
|
|
||||||
|
Connect to a server, register an account, and start chatting.
|
||||||
|
|
||||||
|
## Build from Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/EchoHub.slnx
|
||||||
|
```
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
- name: Getting Started
|
||||||
|
href: getting-started.md
|
||||||
|
- name: Architecture
|
||||||
|
href: architecture.md
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
- name: v0.1.0
|
||||||
|
href: v0.1.0.md
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# v0.1.0 - Initial Release
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Real-time messaging via SignalR
|
||||||
|
- JWT authentication with access and refresh tokens
|
||||||
|
- Channel management (create, set topic, delete)
|
||||||
|
- File and image uploads with magic-byte validation
|
||||||
|
- Image-to-ASCII conversion for terminal display
|
||||||
|
- Presence tracking (online, away, DND, invisible)
|
||||||
|
- 6 built-in TUI themes
|
||||||
|
- Rate limiting on auth, upload, and general endpoints
|
||||||
|
- Self-contained binary releases for Windows, Linux, and macOS
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"metadata": [
|
||||||
|
{
|
||||||
|
"src": [
|
||||||
|
{
|
||||||
|
"src": "../src",
|
||||||
|
"files": [
|
||||||
|
"EchoHub.Core/EchoHub.Core.csproj",
|
||||||
|
"EchoHub.Server/EchoHub.Server.csproj",
|
||||||
|
"EchoHub.Client/EchoHub.Client.csproj"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dest": "api",
|
||||||
|
"properties": {
|
||||||
|
"TargetFramework": "net10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.{md,yml}"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"_site/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resource": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"images/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": "_site",
|
||||||
|
"template": [
|
||||||
|
"default",
|
||||||
|
"modern"
|
||||||
|
],
|
||||||
|
"globalMetadata": {
|
||||||
|
"_appName": "EchoHub",
|
||||||
|
"_appTitle": "EchoHub Documentation",
|
||||||
|
"_enableSearch": true,
|
||||||
|
"_disableContribution": false,
|
||||||
|
"_gitContribute": {
|
||||||
|
"repo": "https://github.com/HueByte/EchoHub",
|
||||||
|
"branch": "master"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"markdownEngineName": "markdig"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
_layout: landing
|
||||||
|
---
|
||||||
|
|
||||||
|
# EchoHub Documentation
|
||||||
|
|
||||||
|
Welcome to the EchoHub documentation. EchoHub is a decentralized, IRC-like chat application built with .NET 10 and SignalR.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Getting Started](articles/getting-started.md) - Set up and run EchoHub
|
||||||
|
- [Architecture](articles/architecture.md) - Understand the system design
|
||||||
|
- [API Reference](api/index.md) - Generated C# API documentation
|
||||||
|
- [Changelog](changelog/v0.1.0.md) - Release history
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
- name: Articles
|
||||||
|
href: articles/
|
||||||
|
- name: Changelog
|
||||||
|
href: changelog/
|
||||||
|
- name: API Reference
|
||||||
|
href: api/
|
||||||
|
homepage: api/index.md
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>0.1.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -181,6 +181,237 @@ public static class ThemeManager
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly Theme DraculaTheme = new()
|
||||||
|
{
|
||||||
|
Name = "Dracula",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "Magenta"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightMagenta",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightMagenta"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "DarkGray",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "Magenta"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightMagenta",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "BrightMagenta",
|
||||||
|
FocusBackground = "Black"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Theme MonokaiTheme = new()
|
||||||
|
{
|
||||||
|
Name = "Monokai",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightYellow"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightYellow"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "DarkGray",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightYellow"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "BrightYellow",
|
||||||
|
FocusBackground = "Black"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Theme NordTheme = new()
|
||||||
|
{
|
||||||
|
Name = "Nord",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "DarkBlue",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "Blue"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "Blue",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "DarkBlue"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "Blue",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "DarkBlue"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightCyan",
|
||||||
|
Background = "DarkBlue",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "DarkBlue"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Theme GruvboxTheme = new()
|
||||||
|
{
|
||||||
|
Name = "Gruvbox",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "DarkYellow"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "DarkYellow"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "DarkGray",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "DarkYellow"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "DarkYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "DarkYellow",
|
||||||
|
FocusBackground = "Black"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Theme OceanTheme = new()
|
||||||
|
{
|
||||||
|
Name = "Ocean",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightCyan",
|
||||||
|
Background = "DarkBlue",
|
||||||
|
FocusForeground = "White",
|
||||||
|
FocusBackground = "DarkCyan"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "DarkCyan",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "DarkBlue"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "DarkCyan",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "DarkBlue"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightCyan",
|
||||||
|
Background = "DarkCyan",
|
||||||
|
FocusForeground = "BrightCyan",
|
||||||
|
FocusBackground = "DarkCyan"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Theme HighContrastTheme = new()
|
||||||
|
{
|
||||||
|
Name = "HighContrast",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightYellow"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightYellow"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "BrightYellow"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "BrightYellow",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "BrightYellow",
|
||||||
|
FocusBackground = "Black"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Theme RosePineTheme = new()
|
||||||
|
{
|
||||||
|
Name = "RosePine",
|
||||||
|
Base = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Black",
|
||||||
|
FocusBackground = "DarkMagenta"
|
||||||
|
},
|
||||||
|
Menu = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "Magenta",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "White",
|
||||||
|
FocusBackground = "DarkMagenta"
|
||||||
|
},
|
||||||
|
Dialog = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "White",
|
||||||
|
Background = "DarkGray",
|
||||||
|
FocusForeground = "White",
|
||||||
|
FocusBackground = "DarkMagenta"
|
||||||
|
},
|
||||||
|
Status = new ThemeColors
|
||||||
|
{
|
||||||
|
Foreground = "Magenta",
|
||||||
|
Background = "Black",
|
||||||
|
FocusForeground = "Magenta",
|
||||||
|
FocusBackground = "Black"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly Theme TransparentTheme = new()
|
private static readonly Theme TransparentTheme = new()
|
||||||
{
|
{
|
||||||
Name = "Transparent",
|
Name = "Transparent",
|
||||||
@@ -221,7 +452,14 @@ public static class ThemeManager
|
|||||||
ClassicTheme,
|
ClassicTheme,
|
||||||
LightTheme,
|
LightTheme,
|
||||||
HackerTheme,
|
HackerTheme,
|
||||||
SolarizedTheme
|
SolarizedTheme,
|
||||||
|
DraculaTheme,
|
||||||
|
MonokaiTheme,
|
||||||
|
NordTheme,
|
||||||
|
GruvboxTheme,
|
||||||
|
OceanTheme,
|
||||||
|
HighContrastTheme,
|
||||||
|
RosePineTheme
|
||||||
];
|
];
|
||||||
|
|
||||||
public static List<Theme> GetAvailableThemes()
|
public static List<Theme> GetAvailableThemes()
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
|
||||||
|
<PackageReference Include="xunit" Version="2.*" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.*">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EchoHub.Core\EchoHub.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\EchoHub.Server\EchoHub.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using EchoHub.Server.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace EchoHub.Tests;
|
||||||
|
|
||||||
|
public class FileValidationHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsValidImage_JpegMagicBytes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
byte[] jpeg = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
|
||||||
|
using var stream = new MemoryStream(jpeg);
|
||||||
|
Assert.True(FileValidationHelper.IsValidImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidImage_PngMagicBytes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
byte[] png = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||||
|
using var stream = new MemoryStream(png);
|
||||||
|
Assert.True(FileValidationHelper.IsValidImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidImage_GifMagicBytes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
byte[] gif = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
|
||||||
|
using var stream = new MemoryStream(gif);
|
||||||
|
Assert.True(FileValidationHelper.IsValidImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidImage_WebpMagicBytes_ReturnsTrue()
|
||||||
|
{
|
||||||
|
byte[] webp = [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50];
|
||||||
|
using var stream = new MemoryStream(webp);
|
||||||
|
Assert.True(FileValidationHelper.IsValidImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidImage_RandomBytes_ReturnsFalse()
|
||||||
|
{
|
||||||
|
byte[] random = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
|
||||||
|
using var stream = new MemoryStream(random);
|
||||||
|
Assert.False(FileValidationHelper.IsValidImage(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidImage_ResetsStreamPosition()
|
||||||
|
{
|
||||||
|
byte[] jpeg = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
|
||||||
|
using var stream = new MemoryStream(jpeg);
|
||||||
|
FileValidationHelper.IsValidImage(stream);
|
||||||
|
Assert.Equal(0, stream.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using EchoHub.Server.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace EchoHub.Tests;
|
||||||
|
|
||||||
|
public class PresenceTrackerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UserConnected_IsOnline_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var tracker = new PresenceTracker();
|
||||||
|
tracker.UserConnected("conn1", Guid.NewGuid(), "alice");
|
||||||
|
Assert.True(tracker.IsOnline("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UserDisconnected_LastConnection_IsOnlineReturnsFalse()
|
||||||
|
{
|
||||||
|
var tracker = new PresenceTracker();
|
||||||
|
tracker.UserConnected("conn1", Guid.NewGuid(), "alice");
|
||||||
|
tracker.UserDisconnected("conn1");
|
||||||
|
Assert.False(tracker.IsOnline("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleConnections_DisconnectOne_StillOnline()
|
||||||
|
{
|
||||||
|
var tracker = new PresenceTracker();
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
tracker.UserConnected("conn1", userId, "alice");
|
||||||
|
tracker.UserConnected("conn2", userId, "alice");
|
||||||
|
tracker.UserDisconnected("conn1");
|
||||||
|
Assert.True(tracker.IsOnline("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JoinChannel_GetOnlineUsersInChannel_ReturnsUser()
|
||||||
|
{
|
||||||
|
var tracker = new PresenceTracker();
|
||||||
|
tracker.UserConnected("conn1", Guid.NewGuid(), "alice");
|
||||||
|
tracker.JoinChannel("alice", "general");
|
||||||
|
var users = tracker.GetOnlineUsersInChannel("general");
|
||||||
|
Assert.Contains("alice", users);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LeaveChannel_UserNoLongerInChannel()
|
||||||
|
{
|
||||||
|
var tracker = new PresenceTracker();
|
||||||
|
tracker.UserConnected("conn1", Guid.NewGuid(), "alice");
|
||||||
|
tracker.JoinChannel("alice", "general");
|
||||||
|
tracker.LeaveChannel("alice", "general");
|
||||||
|
var users = tracker.GetOnlineUsersInChannel("general");
|
||||||
|
Assert.DoesNotContain("alice", users);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetChannelsForUser_ReturnsJoinedChannels()
|
||||||
|
{
|
||||||
|
var tracker = new PresenceTracker();
|
||||||
|
tracker.UserConnected("conn1", Guid.NewGuid(), "alice");
|
||||||
|
tracker.JoinChannel("alice", "general");
|
||||||
|
tracker.JoinChannel("alice", "random");
|
||||||
|
var channels = tracker.GetChannelsForUser("alice");
|
||||||
|
Assert.Contains("general", channels);
|
||||||
|
Assert.Contains("random", channels);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using EchoHub.Core.Constants;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace EchoHub.Tests;
|
||||||
|
|
||||||
|
public class ValidationConstantsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("alice", true)]
|
||||||
|
[InlineData("Bob_123", true)]
|
||||||
|
[InlineData("user-name", true)]
|
||||||
|
[InlineData("abc", true)]
|
||||||
|
[InlineData("ab", false)]
|
||||||
|
[InlineData("", false)]
|
||||||
|
[InlineData("has space", false)]
|
||||||
|
[InlineData("has@symbol", false)]
|
||||||
|
public void UsernameRegex_ValidatesCorrectly(string input, bool expected)
|
||||||
|
{
|
||||||
|
var result = ValidationConstants.UsernameRegex().IsMatch(input);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("general", true)]
|
||||||
|
[InlineData("my-channel_01", true)]
|
||||||
|
[InlineData("ab", true)]
|
||||||
|
[InlineData("a", false)]
|
||||||
|
[InlineData("has space", false)]
|
||||||
|
public void ChannelNameRegex_ValidatesCorrectly(string input, bool expected)
|
||||||
|
{
|
||||||
|
var result = ValidationConstants.ChannelNameRegex().IsMatch(input);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("#FF0000", true)]
|
||||||
|
[InlineData("#aabbcc", true)]
|
||||||
|
[InlineData("FF0000", false)]
|
||||||
|
[InlineData("#FFF", false)]
|
||||||
|
[InlineData("#GGGGGG", false)]
|
||||||
|
public void HexColorRegex_ValidatesCorrectly(string input, bool expected)
|
||||||
|
{
|
||||||
|
var result = ValidationConstants.HexColorRegex().IsMatch(input);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,5 @@
|
|||||||
<Project Path="EchoHub.Client/EchoHub.Client.csproj" />
|
<Project Path="EchoHub.Client/EchoHub.Client.csproj" />
|
||||||
<Project Path="EchoHub.Core/EchoHub.Core.csproj" />
|
<Project Path="EchoHub.Core/EchoHub.Core.csproj" />
|
||||||
<Project Path="EchoHub.Server/EchoHub.Server.csproj" />
|
<Project Path="EchoHub.Server/EchoHub.Server.csproj" />
|
||||||
|
<Project Path="EchoHub.Tests/EchoHub.Tests.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Reference in New Issue
Block a user