diff --git a/MyChat/MyChat.Tests/ArgumentsTests.cs b/MyChat/MyChat.Tests/ArgumentsTests.cs new file mode 100644 index 0000000..694e544 --- /dev/null +++ b/MyChat/MyChat.Tests/ArgumentsTests.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace MyChat.Test; + +using MyChat; +using NUnit.Framework; + +/// +/// Unit tests for parsing logic. +/// +public class ArgumentsTests +{ + /// + /// Tests that parsing a single argument returns server mode. + /// + [Test] + public void Arguments_Parse_SingleArgument_ReturnsServerMode() + { + var args = new[] { "5000" }; + var parsed = Arguments.Parse(args); + + Assert.Multiple(() => + { + Assert.That(parsed.IsServer, Is.True); + Assert.That(parsed.Port, Is.EqualTo(5000)); + Assert.That(parsed.IpAddress, Is.Null); + }); + } + + /// + /// Tests that parsing two arguments returns client mode. + /// + [Test] + public void Arguments_Parse_TwoArguments_ReturnsClientMode() + { + var args = new[] { "127.0.0.1", "5000" }; + var parsed = Arguments.Parse(args); + + Assert.Multiple(() => + { + Assert.That(parsed.IsServer, Is.False); + Assert.That(parsed.Port, Is.EqualTo(5000)); + Assert.That(parsed.IpAddress, Is.EqualTo("127.0.0.1")); + }); + } +} \ No newline at end of file diff --git a/MyChat/MyChat.Tests/ChatRunnerTests.cs b/MyChat/MyChat.Tests/ChatRunnerTests.cs new file mode 100644 index 0000000..e293bca --- /dev/null +++ b/MyChat/MyChat.Tests/ChatRunnerTests.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace MyChat.Test; + +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using MyChat; +using NUnit.Framework; + +/// +/// Tests for . +/// +[TestFixture] +public class ChatRunnerTests +{ + /// + /// Tests that a server and client can exchange a single message. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task ChatRunner_RunAsync_ServerReceive() + { + const string ipAddress = "127.0.0.1"; + const int port = 5005; + + string serverReceived = string.Empty; + string clientMessage = "Hello!"; + + var serverTask = Task.Run(async () => + { + var listener = new TcpListener(IPAddress.Parse(ipAddress), port); + listener.Start(); + + using var client = await listener.AcceptTcpClientAsync(); + using var stream = client.GetStream(); + using var reader = new StreamReader(stream); + using var writer = new StreamWriter(stream) { AutoFlush = true }; + + serverReceived = await reader.ReadLineAsync() ?? string.Empty; + await writer.WriteLineAsync(serverReceived); + + listener.Stop(); + }); + + await Task.Delay(200); + + var clientTask = Task.Run(async () => + { + using var client = new TcpClient(); + await client.ConnectAsync(IPAddress.Parse(ipAddress), port); + using var stream = client.GetStream(); + using var reader = new StreamReader(stream); + using var writer = new StreamWriter(stream) { AutoFlush = true }; + + await writer.WriteLineAsync(clientMessage); + var received = await reader.ReadLineAsync()!; + return received; + }); + + var clientReceived = await clientTask; + await serverTask; + + Assert.Multiple(() => + { + Assert.That(serverReceived, Is.EqualTo(clientMessage)); + Assert.That(clientReceived, Is.EqualTo(clientMessage)); + }); + } +} diff --git a/MyChat/MyChat.Tests/MyChat.Tests.csproj b/MyChat/MyChat.Tests/MyChat.Tests.csproj new file mode 100644 index 0000000..34334e1 --- /dev/null +++ b/MyChat/MyChat.Tests/MyChat.Tests.csproj @@ -0,0 +1,39 @@ + + + + net9.0 + latest + enable + enable + false + true + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/MyChat/MyChat.Tests/stylecop.json b/MyChat/MyChat.Tests/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/MyChat/MyChat.Tests/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file diff --git a/MyChat/MyChat.sln b/MyChat/MyChat.sln new file mode 100644 index 0000000..0692ba5 --- /dev/null +++ b/MyChat/MyChat.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyChat", "MyChat\MyChat.csproj", "{DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyChat.Tests", "MyChat.Tests\MyChat.Tests.csproj", "{A801DB41-1484-46AA-9D09-0CFEB18C7271}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE4B1FDD-7626-40A3-B323-CE8B371EA5E1}.Release|Any CPU.Build.0 = Release|Any CPU + {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A801DB41-1484-46AA-9D09-0CFEB18C7271}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MyChat/MyChat/Arguments.cs b/MyChat/MyChat/Arguments.cs new file mode 100644 index 0000000..6462cbd --- /dev/null +++ b/MyChat/MyChat/Arguments.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace MyChat; + +using System; + +/// +/// Represents parsed command-line arguments for the chat application. +/// +public class Arguments +{ + private Arguments(int port, string? ipAddress) + { + this.Port = port; + this.IpAddress = ipAddress; + } + + /// + /// Gets the network port number. + /// + public int Port { get; } + + /// + /// Gets the IP address for client mode. + /// + public string? IpAddress { get; } + + /// + /// Gets a value indicating whether returns the value, is it a server or an application. + /// + public bool IsServer => this.IpAddress is null; + + /// + /// Parses command-line arguments. + /// + /// Command-line arguments. + /// Parsed instance. + public static Arguments Parse(string[] args) + { + if (args.Length == 1) + { + return new Arguments(int.Parse(args[0]), null); + } + + if (args.Length == 2) + { + return new Arguments(int.Parse(args[1]), args[0]); + } + + throw new ArgumentException("Invalid number of command-line arguments."); + } +} diff --git a/MyChat/MyChat/ChatRunner.cs b/MyChat/MyChat/ChatRunner.cs new file mode 100644 index 0000000..a1b6c17 --- /dev/null +++ b/MyChat/MyChat/ChatRunner.cs @@ -0,0 +1,133 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +namespace MyChat; + +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +/// +/// Provides methods for running a chat application in server or client mode. +/// +public static class ChatRunner +{ + private static readonly List ConnectedClients = []; + + /// + /// Starts the chat application based on the provided arguments. + /// + /// Arguments containing port and optional IP address. + /// A representing the async operation. + public static async Task RunAsync(Arguments arguments) + { + if (arguments.IsServer) + { + await RunServerAsync(arguments.Port); + } + else + { + await RunClientAsync(arguments.IpAddress!, arguments.Port); + } + } + + /// + /// Runs the chat application as a server listening. + /// + /// Port number to listen on. + /// Representing the asynchronous server operation. + public static async Task RunServerAsync(int port) + { + var listener = new TcpListener(IPAddress.Any, port); + listener.Start(); + Console.WriteLine($"Server listening on port {port}..."); + + while (true) + { + var client = await listener.AcceptTcpClientAsync(); + lock (ConnectedClients) + { + ConnectedClients.Add(client); + } + + var endpoint = client.Client.RemoteEndPoint?.ToString() ?? "Unknown"; + + Console.WriteLine($"Client connected: {endpoint}"); + + _ = Task.Run(async () => + { + await RunChatAsync(client, endpoint); + + Console.WriteLine($"Client disconnected: {endpoint}"); + lock (ConnectedClients) + { + ConnectedClients.Remove(client); + } + + lock (ConnectedClients) + { + if (ConnectedClients.Count == 0) + { + Console.WriteLine("No clients connected. Server shutting down."); + Environment.Exit(0); + } + } + }); + } + } + + /// + /// Runs the chat application as a client. + /// + /// IP address of the server to connect to. + /// Port number of the server. + /// Representing the async client operation. + public static async Task RunClientAsync(string ip, int port) + { + using var client = new TcpClient(); + await client.ConnectAsync(ip, port); + Console.WriteLine($"Connected to server {ip}:{port}"); + await RunChatAsync(client, "Server"); + } + + private static async Task RunChatAsync(TcpClient client, string name) + { + using var stream = client.GetStream(); + using var reader = new StreamReader(stream); + using var writer = new StreamWriter(stream) { AutoFlush = true }; + + var consoleTask = Task.Run(async () => + { + while (true) + { + var line = Console.ReadLine(); + if (line == null || line.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + await writer.WriteLineAsync(line); + } + }); + + var networkTask = Task.Run(async () => + { + while (true) + { + var line = await reader.ReadLineAsync(); + if (line == null) + { + break; + } + + Console.WriteLine($"> {line}"); + } + }); + + await Task.WhenAny(consoleTask, networkTask); + client.Close(); + } +} diff --git a/MyChat/MyChat/MyChat.csproj b/MyChat/MyChat/MyChat.csproj new file mode 100644 index 0000000..4f74535 --- /dev/null +++ b/MyChat/MyChat/MyChat.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/MyChat/MyChat/Program.cs b/MyChat/MyChat/Program.cs new file mode 100644 index 0000000..de02506 --- /dev/null +++ b/MyChat/MyChat/Program.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) khusainovilas. All rights reserved. +// + +using MyChat; + +try +{ + var arguments = Arguments.Parse(args); + await ChatRunner.RunAsync(arguments); +} +catch (Exception ex) +{ + Console.WriteLine($"Error: {ex.Message}"); +} diff --git a/MyChat/MyChat/stylecop.json b/MyChat/MyChat/stylecop.json new file mode 100644 index 0000000..76c8e76 --- /dev/null +++ b/MyChat/MyChat/stylecop.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "khusainovilas", + "copyrightText": "Copyright (c) {companyName}. All rights reserved." + } + } +} \ No newline at end of file