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-*
|
||||
src/EchoHub.Server/uploads/*
|
||||
.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
|
||||
|
||||
- **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.
|
||||
- **Colored nicknames** — pick your hex color, express yourself
|
||||
- **File/image sharing** — local files or URLs
|
||||
@@ -151,6 +151,13 @@ dotnet build src/EchoHub.slnx
|
||||
| `light` | Black on white — for the brave |
|
||||
| `hacker` | Green on black — *I'm in* |
|
||||
| `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
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
Name = "Transparent",
|
||||
@@ -221,7 +452,14 @@ public static class ThemeManager
|
||||
ClassicTheme,
|
||||
LightTheme,
|
||||
HackerTheme,
|
||||
SolarizedTheme
|
||||
SolarizedTheme,
|
||||
DraculaTheme,
|
||||
MonokaiTheme,
|
||||
NordTheme,
|
||||
GruvboxTheme,
|
||||
OceanTheme,
|
||||
HighContrastTheme,
|
||||
RosePineTheme
|
||||
];
|
||||
|
||||
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.Core/EchoHub.Core.csproj" />
|
||||
<Project Path="EchoHub.Server/EchoHub.Server.csproj" />
|
||||
<Project Path="EchoHub.Tests/EchoHub.Tests.csproj" />
|
||||
</Solution>
|
||||
|
||||
Reference in New Issue
Block a user