diff --git a/ClientConsole/App.config b/ClientConsole/App.config new file mode 100644 index 00000000..09cc310f --- /dev/null +++ b/ClientConsole/App.config @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ClientConsole/ClientConsole.csproj b/ClientConsole/ClientConsole.csproj new file mode 100644 index 00000000..96665ac9 --- /dev/null +++ b/ClientConsole/ClientConsole.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {CDB23559-BB93-4946-9802-9222A6586013} + Exe + ClientConsole + ClientConsole + v4.6.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8} + SoftWriters.RestaurantReviews.DataLibrary + + + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07} + SoftWriters.RestaurantReviews.WebApi + + + + \ No newline at end of file diff --git a/ClientConsole/Program.cs b/ClientConsole/Program.cs new file mode 100644 index 00000000..684421fe --- /dev/null +++ b/ClientConsole/Program.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SoftWriters.RestaurantReviews.DataLibrary; + +namespace ClientConsole +{ + // Since unit tests test the API pretty well, we only use this console client to make sure the plumbing + // is in place and functions correctly. We don't necessarily need complete functionality here but you can + // go ahead and add functionality as needed. + // NOTE: This is good for say, a working demo before UI gets created. + class Program + { + private static string Instructions = + "\nType the appropriate number that corresponds to the api call you would like to test, then press ENTER:" + + "\n\t0\tExit" + + "\n\t1\tGet Restaurants By City" + + "\n\t2\tGet Restaurants By Zip" + + "\n\t3\tGet All Restaurants" + + "\n\t4\tAdd Restaurant" + + "\n\t5\tAdd Review" + + "\n\t6\tGet Reviews by user" + + "\n\t7\tGet Reviews by restaurant"; + + // We don't have any concept of logging in, so this will represent the current user + private static Guid UserId = Guid.Parse("611C77C4-DA99-4674-8252-87C9923A47D3"); + + static void Main(string[] args) + { + var reviewClient = new ReviewClient(); + Console.WriteLine("Connected!\n"); + Console.WriteLine(Instructions); + + while (true) + { + var input = Console.ReadLine().Trim().ToLower(); + + switch (input) + { + case "0": + case "q": + reviewClient.Close(); + Environment.Exit(0); + break; + + case "1": + GetRestaurantsByCity(reviewClient); + break; + + case "2": + GetRestaurantsByZip(reviewClient); + break; + + case "3": + GetAllRestaurants(reviewClient); + break; + + case "4": + AddRestaurant(reviewClient); + break; + + case "5": + AddReview(reviewClient); + break; + + default: + Console.WriteLine("Invalid input"); + break; + } + + Console.WriteLine(Instructions); + Console.Write(":"); + } + } + + static void GetRestaurantsByCity(ReviewClient reviewClient) + { + Console.Write("Enter city: "); + var city = Console.ReadLine(); + Console.WriteLine(); + var restaurants = reviewClient.GetRestaurants("", city, "", "", ""); + PrintRestaurants(restaurants); + } + + static void GetRestaurantsByZip(ReviewClient reviewClient) + { + Console.Write("Enter zip code: "); + var postalCode = Console.ReadLine(); + Console.WriteLine(); + var restaurants = reviewClient.GetRestaurants("", "", "", postalCode, ""); + PrintRestaurants(restaurants); + } + + static void GetAllRestaurants(ReviewClient reviewClient) + { + Console.WriteLine("Get all restaurants"); + var restaurants = reviewClient.GetRestaurants("", "", "", "", ""); + PrintRestaurants(restaurants); + } + + static void AddRestaurant(ReviewClient reviewClient) + { + Console.Write("Enter restaurant name: "); + var restaurantName = Console.ReadLine(); + Console.Write("\nEnter street address: "); + var restaurantStreet = Console.ReadLine(); + Console.Write("\nEnter city: "); + var restaurantCity = Console.ReadLine(); + Console.Write("\nEnter state: "); + var restaurantState = Console.ReadLine(); + Console.Write("\nEnter postal code: "); + var restaurantZip = Console.ReadLine(); + Console.Write("\nEnter country: "); + var restaurantCountry = Console.ReadLine(); + + bool result = reviewClient.AddRestaurant(restaurantName, restaurantStreet, restaurantCity, restaurantState, + restaurantZip, restaurantCountry); + + Console.WriteLine("\n{0}", result ? "Add successful" : "Add failed"); + } + + static void AddReview(ReviewClient reviewClient) + { + Console.WriteLine("Enter the number corresponding to "); + + var restaurants = reviewClient.GetRestaurants("", "", "", "", "").ToList(); + + TrySelectRestaurant(restaurants, out Restaurant restaurant); + Console.Write("\nEnter overall rating (1-5): "); + var overallRating = int.TryParse(Console.ReadLine(), out int oRating) ? oRating : 0; + + Console.Write("\nEnter food rating (1-5): "); + var foodRating = int.TryParse(Console.ReadLine(), out int fRating) ? fRating : 0; + + Console.Write("\nEnter service rating (1-5): "); + var serviceRating = int.TryParse(Console.ReadLine(), out int sRating) ? sRating : 0; + + Console.Write("\nEnter cost rating (1-5): "); + var costRating = int.TryParse(Console.ReadLine(), out int cRating) ? cRating : 0; + + Console.Write("Enter comments: "); + var comments = Console.ReadLine(); + + bool result = reviewClient.AddReview(UserId, restaurant.Id, overallRating, foodRating, serviceRating, costRating, comments); + + Console.WriteLine("\n{0}", result ? "Add successful" : "Add failed"); + } + + static bool TrySelectRestaurant(List restaurants, out Restaurant selection) + { + selection = restaurants[0]; + + Console.WriteLine("Select restaurant (type the number and press Enter)"); + for (int i = 0; i < restaurants.Count; i++) + { + Console.WriteLine("[{0}]\t{1}", i, restaurants[i].Name); + } + + var input = Console.ReadLine(); + if (!int.TryParse(input, out int index)) + return false; + + if (index >= restaurants.Count) + return false; + + selection = restaurants[index]; + return true; + } + + static void PrintRestaurants(IEnumerable restaurants) + { + foreach (var item in restaurants) + { + Console.WriteLine(item.Name); + } + } + } +} diff --git a/ClientConsole/Properties/AssemblyInfo.cs b/ClientConsole/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..51e0f842 --- /dev/null +++ b/ClientConsole/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ClientConsole")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ClientConsole")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("cdb23559-bb93-4946-9802-9222a6586013")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ClientConsole/ReviewClient.cs b/ClientConsole/ReviewClient.cs new file mode 100644 index 00000000..a90c0018 --- /dev/null +++ b/ClientConsole/ReviewClient.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Channels; +using SoftWriters.RestaurantReviews.DataLibrary; +using SoftWriters.RestaurantReviews.WebApi; + +namespace ClientConsole +{ + public class ReviewClient : ClientBase, IReviewApi + { + public ReviewClient() + { } + + public ReviewClient(Binding binding, EndpointAddress remoteAddress) : base(binding, remoteAddress) + { } + + public IEnumerable GetRestaurants(string street, string city, string stateCode, string postalCode, string country) + { + return Channel.GetRestaurants(street, city, stateCode, postalCode, country); + } + + public IEnumerable GetReviews(Guid userId, Guid restaurantId) + { + return Channel.GetReviews(userId, restaurantId); + } + + public bool AddRestaurant(string name, string street, string city, string stateCode, string postalCode, string country) + { + return Channel.AddRestaurant(name, street, city, stateCode, postalCode, country); + } + + public bool AddReview(Guid userId, Guid restaurantId, int overallRating, int foodRating, int serviceRating, int costRating, + string comments) + { + return Channel.AddReview(userId, restaurantId, overallRating, foodRating, serviceRating, costRating, + comments); + } + + public bool DeleteReview(Guid id) + { + return Channel.DeleteReview(id); + } + } +} diff --git a/CreateTestCerts.ps1 b/CreateTestCerts.ps1 new file mode 100644 index 00000000..a7f6f609 --- /dev/null +++ b/CreateTestCerts.ps1 @@ -0,0 +1,25 @@ +# For creating self-signed test certificates +# NOTE: Needs run as administrator +# NOTE: $dnsName must match the host name of the server \ uri of the service + +$dnsName = "DESKTOP-JJLKN7I" +$rootDnsName = "RootCA for " + $dnsName +$outputDir = "C:\testCertificates\" + $dnsName + "\" +$rootPfxPath = $outputDir + 'root.pfx' +$rootCrtPath = $outputDir + 'root.crt' +$pfxPath = $outputDir + 'cert.pfx' +$crtPath = $outputDir + 'cert.crt' + +New-Item -Path $outputDir -ItemType Directory + +$rootCert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName $rootDnsName -FriendlyName "BryanRootCA" -NotAfter (Get-Date).AddYears(1) -KeyUsage CertSign +[System.Security.SecureString]$rootcertPassword = ConvertTo-SecureString -String "password" -Force -AsPlainText +[String]$rootCertPath = Join-Path -Path 'cert:\LocalMachine\My\' -ChildPath "$($rootcert.Thumbprint)" + +Export-PfxCertificate -Cert $rootCertPath -FilePath $rootPfxPath -Password $rootcertPassword +Export-Certificate -Cert $rootCertPath -FilePath $rootCrtPath + +$testCert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName $dnsName -KeyExportPolicy Exportable -KeyLength 2048 -KeyUsage DigitalSignature,KeyEncipherment -Signer $rootCert +[String]$testCertPath = Join-Path -Path 'cert:\LocalMachine\My\' -ChildPath "$($testCert.Thumbprint)" +Export-PfxCertificate -Cert $testCertPath -FilePath $pfxPath -Password $rootcertPassword +Export-Certificate -Cert $testCertPath -FilePath $crtPath \ No newline at end of file diff --git a/InstallHelper/CustomAction.config b/InstallHelper/CustomAction.config new file mode 100644 index 00000000..c837a2ce --- /dev/null +++ b/InstallHelper/CustomAction.config @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/InstallHelper/CustomAction.cs b/InstallHelper/CustomAction.cs new file mode 100644 index 00000000..4516ba76 --- /dev/null +++ b/InstallHelper/CustomAction.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using Microsoft.Deployment.WindowsInstaller; + +namespace InstallHelper +{ + public class CustomActions + { + [CustomAction] + public static ActionResult UpdateDomainInConfigs(Session session) + { + session.Log("Begin UpdateDomainInConfigs"); + + try + { + // need to pass the data to a deferred custom action as a single property + // so here we are using an '*' to delimit the data + string data = session.CustomActionData["DOMAINCONFIGDATA"]; + var items = data.Split('*'); + var installFolder = items[0]; + + var domain = items[1]; + var serviceConfig = Path.Combine(installFolder, "Service", "SoftWriters.RestaurantReviews.Service.exe.config"); + var clientConfig = Path.Combine(installFolder, "TestClient", "ClientConsole.exe.config"); + + UpdateAppConfigWithDomain(serviceConfig, domain); + UpdateAppConfigWithDomain(clientConfig, domain); + } + catch (Exception e) + { + session.Log("Error updating app.config files with domain: " + e.Message); + return ActionResult.Failure; + } + + return ActionResult.Success; + } + + private static void UpdateAppConfigWithDomain(string filename, string domain) + { + var contents = File.ReadAllText(filename); + contents = contents.Replace("localhost", domain); + File.WriteAllText(filename, contents); + } + } +} diff --git a/InstallHelper/InstallHelper.csproj b/InstallHelper/InstallHelper.csproj new file mode 100644 index 00000000..a8bc168b --- /dev/null +++ b/InstallHelper/InstallHelper.csproj @@ -0,0 +1,54 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {68F6E2E8-E373-4D97-B26B-6D71B3A06FD2} + Library + Properties + InstallHelper + InstallHelper + v4.6.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + True + + + + + + + + + + + + + + \ No newline at end of file diff --git a/InstallHelper/Properties/AssemblyInfo.cs b/InstallHelper/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..9e848365 --- /dev/null +++ b/InstallHelper/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("InstallHelper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("InstallHelper")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("68f6e2e8-e373-4d97-b26b-6d71b3a06fd2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..2c674001 --- /dev/null +++ b/README.txt @@ -0,0 +1,53 @@ +ORGANIZATION ********************************************* +SoftWriters.RestaurantReviews.WebApi +- This is the actual api and implementation + +SoftWriters.RestarantReviews.DataLibrary +- This contains my data objects as well as my implementation for my disk based data stores. + +SoftWriters.RestaurantReviews.Service +- Service to host the web api + +Test +- Directory that contains the following: + - ServiceConsole: Service console, similar to my service, but runs in a console for ease of testing + - ClientConsole: A console app with a client implementation. Good for testing a client, similar to what would + be used in a mobile app. + - SoftWriters.RestaurantReviews.WebApi.Tests: My api unit tests + - CreateTestCerts.ps1: A powershell script to create self-signed test certs, needed for TLS\SSL configuration + +Installer +- Contains a project for the msi and a custom action that updates the app.config files with the appropriate domain (passed via command line along with install folder) +- To run installer: msiexec /i RestaurantReviewsServerMsi.msi DOMAIN= InstallerFolder=C:\some\Path /lvoicewarmupx installerlog.txt + +SETUP ********************************************* +- In order to run the test console apps, you need to Run As Administrator +- If using TLS\SSL, you need to create the certificates, and the certificate must have the host name as the service +- In the console client, just comment/uncomment the appropriate lines to toggle between http and https +- NOTE: In the configs, I only have https endpoints commented out, because I wanted a working solution with an installer, +and so I didn't want to have to worry about certificates. But I tested with https endpoints and it should be all good, if running the ServiceConsole + +DEPENDENCIES ****************************************** +- You will need to restore nuget packages to get the MSTest packages +- For the installer, I have WiX v3.11 installed, and so if you don't have that installed, or the WiX VS extension +just unload the projects under Installer and forget about them + +AREAS OF IMPROVEMENT ********************************************* +- I tried to make my api agnostic to the data source, and rather than connect to a database, for my testing, I created +a disk based data store. However, there could probably be improvements. My datastore isn't very efficient. Also, if +connecting to a proper database, my api could be tweaked a little, such as using ints for ids rather than guids. + +- My app doesn't really have a concept of a logged in user. Some calls I pass in a user id, and some I make an assumption +about who the user is. + +- I created unit tests for my api, but not my xml data stores. As I said, if I were going via a database, I wouldn't be using +these data stores. But, if I were going with the xml data stores in the long run, then I should probably have some unit tests +created for those as well. + +- Once I got my API working, I thought I would be fancy and include and installer project, complete with install bootstrapper. I didn't create the installer UI +because that would take a lot of time to do right and I wouldn't want to do it if it wasn't right. But if you're going to have the installer, it would be good +to have a bootstrapper that defaults the domain to that of the target machine. + +- I targeted .NET 4.6.1, because I worked on a web api project recently that also targeted that .NET version, and so was most comfortable with that version. +I believe I had read that in more recent .NET versions, they handle certain things a little differently. I can't remember what that is, but I would assume that +if targeting a recent version of .NET Framework, one would have to do at least some minimal rework to get everything running properly. \ No newline at end of file diff --git a/RestaurantReviews.sln b/RestaurantReviews.sln new file mode 100644 index 00000000..740beb32 --- /dev/null +++ b/RestaurantReviews.sln @@ -0,0 +1,86 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.1401 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoftWriters.RestaurantReviews.DataLibrary", "SoftWriters.RestaurantReviews.DataLibrary\SoftWriters.RestaurantReviews.DataLibrary.csproj", "{F2472982-DC7E-4EB8-86DD-45F1E3D189E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoftWriters.RestaurantReviews.Service", "SoftWriters.RestaurantReviews.Service\SoftWriters.RestaurantReviews.Service.csproj", "{E7FF0558-C73D-4F0A-B56C-E5C595A87E41}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoftWriters.RestaurantReviews.WebApi", "SoftWriters.RestaurantReviews.WebApi\SoftWriters.RestaurantReviews.WebApi.csproj", "{4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceConsole", "ServiceConsole\ServiceConsole.csproj", "{C87A79FA-E477-4E8A-87A5-7DAB90F6F50A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{1782B4F8-CF3F-4C00-B854-DF5302BFAEEB}" + ProjectSection(SolutionItems) = preProject + CreateTestCerts.ps1 = CreateTestCerts.ps1 + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClientConsole", "ClientConsole\ClientConsole.csproj", "{CDB23559-BB93-4946-9802-9222A6586013}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B07E82E1-2671-4549-9F5E-2FC5C94E8835}" + ProjectSection(SolutionItems) = preProject + README.txt = README.txt + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Installer", "Installer", "{D4973E9C-9598-4511-8BBC-7F14936FFE10}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SoftWriters.RestaurantReviews.WebApi.Tests", "SoftWriters.RestaurantReviews.WebApi.Tests\SoftWriters.RestaurantReviews.WebApi.Tests.csproj", "{B36A5C8D-5C18-47E9-A3D9-070FD70706AD}" +EndProject +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "RestaurantReviewsServerMsi", "RestaurantReviewsServerMsi\RestaurantReviewsServerMsi.wixproj", "{44924695-C006-4D5A-880B-530EE78B1860}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstallHelper", "InstallHelper\InstallHelper.csproj", "{68F6E2E8-E373-4D97-B26B-6D71B3A06FD2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8}.Release|Any CPU.Build.0 = Release|Any CPU + {E7FF0558-C73D-4F0A-B56C-E5C595A87E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7FF0558-C73D-4F0A-B56C-E5C595A87E41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7FF0558-C73D-4F0A-B56C-E5C595A87E41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7FF0558-C73D-4F0A-B56C-E5C595A87E41}.Release|Any CPU.Build.0 = Release|Any CPU + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07}.Release|Any CPU.Build.0 = Release|Any CPU + {C87A79FA-E477-4E8A-87A5-7DAB90F6F50A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C87A79FA-E477-4E8A-87A5-7DAB90F6F50A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C87A79FA-E477-4E8A-87A5-7DAB90F6F50A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C87A79FA-E477-4E8A-87A5-7DAB90F6F50A}.Release|Any CPU.Build.0 = Release|Any CPU + {CDB23559-BB93-4946-9802-9222A6586013}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDB23559-BB93-4946-9802-9222A6586013}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDB23559-BB93-4946-9802-9222A6586013}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDB23559-BB93-4946-9802-9222A6586013}.Release|Any CPU.Build.0 = Release|Any CPU + {B36A5C8D-5C18-47E9-A3D9-070FD70706AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B36A5C8D-5C18-47E9-A3D9-070FD70706AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B36A5C8D-5C18-47E9-A3D9-070FD70706AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B36A5C8D-5C18-47E9-A3D9-070FD70706AD}.Release|Any CPU.Build.0 = Release|Any CPU + {44924695-C006-4D5A-880B-530EE78B1860}.Debug|Any CPU.ActiveCfg = Debug|x86 + {44924695-C006-4D5A-880B-530EE78B1860}.Debug|Any CPU.Build.0 = Debug|x86 + {44924695-C006-4D5A-880B-530EE78B1860}.Release|Any CPU.ActiveCfg = Release|x86 + {44924695-C006-4D5A-880B-530EE78B1860}.Release|Any CPU.Build.0 = Release|x86 + {68F6E2E8-E373-4D97-B26B-6D71B3A06FD2}.Debug|Any CPU.ActiveCfg = Debug|x86 + {68F6E2E8-E373-4D97-B26B-6D71B3A06FD2}.Debug|Any CPU.Build.0 = Debug|x86 + {68F6E2E8-E373-4D97-B26B-6D71B3A06FD2}.Release|Any CPU.ActiveCfg = Release|x86 + {68F6E2E8-E373-4D97-B26B-6D71B3A06FD2}.Release|Any CPU.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C87A79FA-E477-4E8A-87A5-7DAB90F6F50A} = {1782B4F8-CF3F-4C00-B854-DF5302BFAEEB} + {CDB23559-BB93-4946-9802-9222A6586013} = {1782B4F8-CF3F-4C00-B854-DF5302BFAEEB} + {B36A5C8D-5C18-47E9-A3D9-070FD70706AD} = {1782B4F8-CF3F-4C00-B854-DF5302BFAEEB} + {44924695-C006-4D5A-880B-530EE78B1860} = {D4973E9C-9598-4511-8BBC-7F14936FFE10} + {68F6E2E8-E373-4D97-B26B-6D71B3A06FD2} = {D4973E9C-9598-4511-8BBC-7F14936FFE10} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6930CFF7-48FD-410E-A18D-6DE53E1F131D} + EndGlobalSection +EndGlobal diff --git a/RestaurantReviewsServerMsi/Product.wxs b/RestaurantReviewsServerMsi/Product.wxs new file mode 100644 index 00000000..7162df64 --- /dev/null +++ b/RestaurantReviewsServerMsi/Product.wxs @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + NOT (REMOVE ~= "ALL") OR WIX_UPGRADE_DETECTED + NOT (REMOVE ~= "ALL") OR WIX_UPGRADE_DETECTED + + + + + diff --git a/RestaurantReviewsServerMsi/RestaurantReviewsServerMsi.wixproj b/RestaurantReviewsServerMsi/RestaurantReviewsServerMsi.wixproj new file mode 100644 index 00000000..b1861733 --- /dev/null +++ b/RestaurantReviewsServerMsi/RestaurantReviewsServerMsi.wixproj @@ -0,0 +1,90 @@ + + + + Debug + x86 + 3.10 + 44924695-c006-4d5a-880b-530ee78b1860 + 2.0 + RestaurantReviewsServerMsi + Package + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + + + + + + ClientConsole + {cdb23559-bb93-4946-9802-9222a6586013} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + InstallHelper + {68f6e2e8-e373-4d97-b26b-6d71b3a06fd2} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + SoftWriters.RestaurantReviews.DataLibrary + {f2472982-dc7e-4eb8-86dd-45f1e3d189e8} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + SoftWriters.RestaurantReviews.Service + {e7ff0558-c73d-4f0a-b56c-e5c595a87e41} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + SoftWriters.RestaurantReviews.WebApi + {4f38eb50-bb3a-44e7-9fe2-3600fcc0be07} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + $(WixExtDir)\WixUtilExtension.dll + WixUtilExtension + + + + + + + + + \ No newline at end of file diff --git a/RestaurantReviewsServerMsi/config.wxi b/RestaurantReviewsServerMsi/config.wxi new file mode 100644 index 00000000..08f559b1 --- /dev/null +++ b/RestaurantReviewsServerMsi/config.wxi @@ -0,0 +1,5 @@ + + + + + diff --git a/RestaurantReviewsServerMsi/content.wxs b/RestaurantReviewsServerMsi/content.wxs new file mode 100644 index 00000000..034f2007 --- /dev/null +++ b/RestaurantReviewsServerMsi/content.wxs @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RestaurantReviewsServerMsi/directoryStructure.wxs b/RestaurantReviewsServerMsi/directoryStructure.wxs new file mode 100644 index 00000000..b966ca13 --- /dev/null +++ b/RestaurantReviewsServerMsi/directoryStructure.wxs @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ServiceConsole/App.config b/ServiceConsole/App.config new file mode 100644 index 00000000..7bcccbe8 --- /dev/null +++ b/ServiceConsole/App.config @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ServiceConsole/Program.cs b/ServiceConsole/Program.cs new file mode 100644 index 00000000..1556d7b1 --- /dev/null +++ b/ServiceConsole/Program.cs @@ -0,0 +1,21 @@ +using System; +using System.ServiceModel.Web; +using SoftWriters.RestaurantReviews.WebApi; + +namespace ServiceConsole +{ + // For testing and debugging + + class Program + { + static void Main(string[] args) + { + var serviceHost = new WebServiceHost(typeof(ReviewApi)); + serviceHost.Open(); + + Console.WriteLine("HTTP Service is running. Press any key to quit..."); + Console.ReadKey(); + serviceHost.Close(); + } + } +} diff --git a/ServiceConsole/Properties/AssemblyInfo.cs b/ServiceConsole/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e649c61d --- /dev/null +++ b/ServiceConsole/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ServiceConsole")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ServiceConsole")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c87a79fa-e477-4e8a-87a5-7dab90f6f50a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ServiceConsole/ServiceConsole.csproj b/ServiceConsole/ServiceConsole.csproj new file mode 100644 index 00000000..f947275e --- /dev/null +++ b/ServiceConsole/ServiceConsole.csproj @@ -0,0 +1,64 @@ + + + + + Debug + AnyCPU + {C87A79FA-E477-4E8A-87A5-7DAB90F6F50A} + Exe + ServiceConsole + ServiceConsole + v4.6.1 + 512 + true + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + Designer + + + + + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07} + SoftWriters.RestaurantReviews.WebApi + + + + \ No newline at end of file diff --git a/SoftWriters.RestaurantReviews.DataLibrary/DataObjects.cs b/SoftWriters.RestaurantReviews.DataLibrary/DataObjects.cs new file mode 100644 index 00000000..a8d6fb6c --- /dev/null +++ b/SoftWriters.RestaurantReviews.DataLibrary/DataObjects.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.Serialization; + +namespace SoftWriters.RestaurantReviews.DataLibrary +{ + [DataContract] + public class User + { + [DataMember] public Guid Id { get; set; } + [DataMember] public string Name { get; set; } + [DataMember] public Address Address { get; set; } + + public User() + { } + + public User(Guid id, string name, Address address) + { + Id = id; + Name = name; + Address = address; + } + } + + [DataContract] + public class Address + { + [DataMember] public string StreetAddress { get; set; } + [DataMember] public string PostalCode { get; set; } + [DataMember] public string City { get; set; } + [DataMember] public string StateCode { get; set; } + [DataMember] public string Country { get; set; } + + public Address() + { } + + public Address(string street, string postalCode, string city, string stateCode, string country) + { + StreetAddress = street; + PostalCode = postalCode; + City = city; + StateCode = stateCode; + Country = country; + } + } + + [DataContract] + public class Review + { + [DataMember] public Guid Id { get; set; } + [DataMember] public Guid UserId { get; set; } + [DataMember] public Guid RestaurantId { get; set; } + [DataMember] public int OverallRating { get; set; } + [DataMember] public int FoodRating { get; set; } + [DataMember] public int ServiceRating { get; set; } + [DataMember] public int CostRating { get; set; } + [DataMember] public string Comments { get; set; } + + public Review() + { } + + public Review(Guid id, Guid userId, Guid restaurantId, int overallRating, int foodRating, int serviceRating, int costRating, string comments) + { + Id = id; + UserId = userId; + RestaurantId = restaurantId; + OverallRating = overallRating; + FoodRating = foodRating; + ServiceRating = serviceRating; + CostRating = costRating; + Comments = comments; + } + } + + [DataContract] + public class Restaurant + { + [DataMember] public Guid Id { get; set; } + [DataMember] public string Name { get; set; } + [DataMember] public Address Address { get; set; } + + public Restaurant() + { } + + public Restaurant(Guid id, string name, Address address) + { + Id = id; + Name = name; + Address = address; + } + } +} diff --git a/SoftWriters.RestaurantReviews.DataLibrary/Properties/AssemblyInfo.cs b/SoftWriters.RestaurantReviews.DataLibrary/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e6198068 --- /dev/null +++ b/SoftWriters.RestaurantReviews.DataLibrary/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SoftWriters.RestaurantReviews.DataLibrary")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SoftWriters.RestaurantReviews.DataLibrary")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f2472982-dc7e-4eb8-86dd-45f1e3d189e8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SoftWriters.RestaurantReviews.DataLibrary/SoftWriters.RestaurantReviews.DataLibrary.csproj b/SoftWriters.RestaurantReviews.DataLibrary/SoftWriters.RestaurantReviews.DataLibrary.csproj new file mode 100644 index 00000000..b1aa571b --- /dev/null +++ b/SoftWriters.RestaurantReviews.DataLibrary/SoftWriters.RestaurantReviews.DataLibrary.csproj @@ -0,0 +1,51 @@ + + + + + Debug + AnyCPU + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8} + Library + Properties + SoftWriters.RestaurantReviews.DataLibrary + SoftWriters.RestaurantReviews.DataLibrary + v4.6.1 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SoftWriters.RestaurantReviews.DataLibrary/XmlDataStore.cs b/SoftWriters.RestaurantReviews.DataLibrary/XmlDataStore.cs new file mode 100644 index 00000000..d01bf8f4 --- /dev/null +++ b/SoftWriters.RestaurantReviews.DataLibrary/XmlDataStore.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Xml; + +namespace SoftWriters.RestaurantReviews.DataLibrary +{ + public interface IDataStore + { + IEnumerable GetAllItems(); + IEnumerable GetItems(Func predicate); + void AddItem(T item); + void DeleteItem(T item); + } + + /// + /// Implements our data store as xml files in the local users documents folder. + /// It's not a great solution, but this would ultimately be stored in a database. + /// Just saving some time implementing a file based solution. + /// + /// + public class XmlDataStore : IDataStore + { + private static List SerializableTypes = new List + { + typeof(T), + typeof(Restaurant), + typeof(List), + typeof(User), + typeof(List), + typeof(Review), + typeof(List), + typeof(Address) + }; + + private readonly string _filename; + private readonly object _lock = new object(); + + public XmlDataStore() + { + string myDocsDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + _filename = Path.Combine(myDocsDir, string.Format("xmlDataStore_{0}.xml", typeof(T).Name)); + EnsureDataExists(); + } + + public IEnumerable GetAllItems() + { + lock (_lock) + { + var items = Deserialize(); + return items; + } + } + + public IEnumerable GetItems(Func predicate) + { + IEnumerable items; + lock (_lock) + { + items = Deserialize(); + } + + return items.Where(predicate); + } + + public void AddItem(T item) + { + lock (_lock) + { + var items = Deserialize().ToList(); + items.Add(item); + Serialize(items); + } + } + + public void DeleteItem(T item) + { + lock (_lock) + { + var items = Deserialize().ToList(); + if(items.Contains(item)) + items.Remove(item); + } + } + + private IEnumerable Deserialize() + { + EnsureDataExists(); + + List items; + var serializer = new DataContractSerializer(typeof(T), SerializableTypes); + using (var reader = XmlReader.Create(_filename)) + { + items = serializer.ReadObject(reader) as List; + } + + return items; + } + + private void EnsureDataExists() + { + if (File.Exists(_filename)) + return; + + Serialize(new List()); + } + + private void Serialize(IEnumerable items) + { + var serializer = new DataContractSerializer(typeof(T), SerializableTypes); + using (var writer = new XmlTextWriter(_filename, Encoding.UTF8)) + { + serializer.WriteObject(writer, items); + } + } + } +} diff --git a/SoftWriters.RestaurantReviews.Service/App.config b/SoftWriters.RestaurantReviews.Service/App.config new file mode 100644 index 00000000..1285d532 --- /dev/null +++ b/SoftWriters.RestaurantReviews.Service/App.config @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SoftWriters.RestaurantReviews.Service/Program.cs b/SoftWriters.RestaurantReviews.Service/Program.cs new file mode 100644 index 00000000..1ba0870a --- /dev/null +++ b/SoftWriters.RestaurantReviews.Service/Program.cs @@ -0,0 +1,16 @@ +using System.ServiceProcess; + +namespace SoftWriters.RestaurantReviews.Service +{ + static class Program + { + static void Main() + { + var servicesToRun = new ServiceBase[] + { + new Service1() + }; + ServiceBase.Run(servicesToRun); + } + } +} diff --git a/SoftWriters.RestaurantReviews.Service/Properties/AssemblyInfo.cs b/SoftWriters.RestaurantReviews.Service/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..cb82a2f7 --- /dev/null +++ b/SoftWriters.RestaurantReviews.Service/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SoftWriters.RestaurantReviews.Service")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SoftWriters.RestaurantReviews.Service")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e7ff0558-c73d-4f0a-b56c-e5c595a87e41")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SoftWriters.RestaurantReviews.Service/Service1.Designer.cs b/SoftWriters.RestaurantReviews.Service/Service1.Designer.cs new file mode 100644 index 00000000..ab506de8 --- /dev/null +++ b/SoftWriters.RestaurantReviews.Service/Service1.Designer.cs @@ -0,0 +1,37 @@ +namespace SoftWriters.RestaurantReviews.Service +{ + partial class Service1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.ServiceName = "Service1"; + } + + #endregion + } +} diff --git a/SoftWriters.RestaurantReviews.Service/Service1.cs b/SoftWriters.RestaurantReviews.Service/Service1.cs new file mode 100644 index 00000000..0ef1040a --- /dev/null +++ b/SoftWriters.RestaurantReviews.Service/Service1.cs @@ -0,0 +1,28 @@ +using System.ServiceModel; +using System.ServiceModel.Web; +using System.ServiceProcess; +using SoftWriters.RestaurantReviews.WebApi; + +namespace SoftWriters.RestaurantReviews.Service +{ + public partial class Service1 : ServiceBase + { + private readonly ServiceHost _serviceHost; + + public Service1() + { + InitializeComponent(); + _serviceHost = new WebServiceHost(typeof(ReviewApi)); + } + + protected override void OnStart(string[] args) + { + _serviceHost.Open(); + } + + protected override void OnStop() + { + _serviceHost?.Close(); + } + } +} diff --git a/SoftWriters.RestaurantReviews.Service/SoftWriters.RestaurantReviews.Service.csproj b/SoftWriters.RestaurantReviews.Service/SoftWriters.RestaurantReviews.Service.csproj new file mode 100644 index 00000000..61bda151 --- /dev/null +++ b/SoftWriters.RestaurantReviews.Service/SoftWriters.RestaurantReviews.Service.csproj @@ -0,0 +1,73 @@ + + + + + Debug + AnyCPU + {E7FF0558-C73D-4F0A-B56C-E5C595A87E41} + WinExe + SoftWriters.RestaurantReviews.Service + SoftWriters.RestaurantReviews.Service + v4.6.1 + 512 + true + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + Component + + + Service1.cs + + + + + + + Designer + + + + + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07} + SoftWriters.RestaurantReviews.WebApi + + + + \ No newline at end of file diff --git a/SoftWriters.RestaurantReviews.WebApi.Tests/GenerateData.cs b/SoftWriters.RestaurantReviews.WebApi.Tests/GenerateData.cs new file mode 100644 index 00000000..6844b113 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi.Tests/GenerateData.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SoftWriters.RestaurantReviews.DataLibrary; + +namespace SoftWriters.RestaurantReviews.WebApi.Tests +{ + // Some unit test methods to generate local data for testing purposes + + [TestClass] + public class GenerateData + { + // Uncomment [TestMethod] attributes on the given methods in order to run + // unit tests which clear and load database with test data + + private List _restaurants; + private List _reviews; + private List _users; + + [TestMethod] + public void GenerateAllData() + { + GenerateRestaurantData(); + GenerateUserData(); + GenerateReviewData(); + } + + //[TestMethod] + public void GenerateRestaurantData() + { + string myDocsDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + var filename = Path.Combine(myDocsDir, string.Format("xmlDataStore_{0}.xml", nameof(Restaurant))); + if(File.Exists(filename)) + File.Delete(filename); + + _restaurants = new List() + { + new Restaurant(Guid.NewGuid(), "Grille 565", new Address("565 Lincoln Ave", "15202", "Pittsburgh", "PA", "United States")), + new Restaurant(Guid.NewGuid(), "202 Hometown Tacos", new Address("202 Lincoln Ave", "15202", "Pittsburgh", "PA", "United States")), + new Restaurant(Guid.NewGuid(), "Bryan's Speakeasy", new Address("205 N Sprague Ave", "15202", "Pittsburgh", "PA", "United States")), + new Restaurant(Guid.NewGuid(), "Katz's Delicatessen", new Address("205 E Houston St", "10002", "New York", "NY", "United States")) + }; + + var dataStore = new XmlDataStore(); + foreach(var item in _restaurants) + dataStore.AddItem(item); + } + + //[TestMethod] + public void GenerateUserData() + { + string myDocsDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + var filename = Path.Combine(myDocsDir, string.Format("xmlDataStore_{0}.xml", nameof(User))); + if (File.Exists(filename)) + File.Delete(filename); + + _users = new List() + { + new User(Guid.NewGuid(), "John Doe", new Address("703 Highland", "16335", "Meadville", "PA", "United States")), + new User(Guid.NewGuid(), "Jane Doe", new Address("12 Marie Ave", "15202", "Pittsburgh", "PA", "United States")) + }; + + var dataStore = new XmlDataStore(); + foreach (var item in _users) + dataStore.AddItem(item); + } + + //[TestMethod] + public void GenerateReviewData() + { + string myDocsDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + var filename = Path.Combine(myDocsDir, string.Format("xmlDataStore_{0}.xml", nameof(Review))); + if (File.Exists(filename)) + File.Delete(filename); + + _reviews = new List() + { + new Review(Guid.NewGuid(), _users[0].Id, _restaurants[0].Id, 4, 4, 4, 3, "Cozy!"), + new Review(Guid.NewGuid(), _users[1].Id, _restaurants[1].Id, 3, 3, 2, 3, "Meh!") + }; + + var dataStore = new XmlDataStore(); + foreach (var item in _reviews) + dataStore.AddItem(item); + } + } +} diff --git a/SoftWriters.RestaurantReviews.WebApi.Tests/Properties/AssemblyInfo.cs b/SoftWriters.RestaurantReviews.WebApi.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..46e1a064 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SoftWriters.RestaurantReviews.WebApi.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SoftWriters.RestaurantReviews.WebApi.Tests")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("b36a5c8d-5c18-47e9-a3d9-070fd70706ad")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SoftWriters.RestaurantReviews.WebApi.Tests/ReviewApiTests.cs b/SoftWriters.RestaurantReviews.WebApi.Tests/ReviewApiTests.cs new file mode 100644 index 00000000..cf301556 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi.Tests/ReviewApiTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SoftWriters.RestaurantReviews.DataLibrary; + +namespace SoftWriters.RestaurantReviews.WebApi.Tests +{ + [TestClass] + public class ReviewApiTests + { + private TestDataStore _restaurantStore; + private TestDataStore _reviewDataStore; + private TestDataStore _userDataStore; + private ReviewApi _reviewApi; + + [TestInitialize] + public void Initialize() + { + var restaurants = new List() + { + new Restaurant(Guid.NewGuid(), "Grille 565", + new Address("565 Lincoln Ave", "15202", "Pittsburgh", "PA", "United States")), + new Restaurant(Guid.NewGuid(), "202 Hometown Tacos", + new Address("202 Lincoln Ave", "15202", "Pittsburgh", "PA", "United States")), + new Restaurant(Guid.NewGuid(), "Bryan's Speakeasy", + new Address("205 N Sprague Ave", "15202", "Pittsburgh", "PA", "United States")), + new Restaurant(Guid.NewGuid(), "Katz's Delicatessen", + new Address("205 E Houston St", "10002", "New York", "NY", "United States")) + }; + + var users = new List() + { + new User(Guid.NewGuid(), "John Doe", + new Address("123 Main", "15202", "Pittsburgh", "PA", "United States")), + new User(Guid.NewGuid(), "Jane Doe", + new Address("123 Main", "15202", "Pittsburgh", "PA", "United States")) + }; + + var reviews = new List() + { + new Review(Guid.NewGuid(), users[0].Id, restaurants[0].Id, 4, 4, 4, 3, "Cozy!"), + new Review(Guid.NewGuid(), users[1].Id, restaurants[1].Id, 3, 3, 3, 3, "meh") + }; + + _restaurantStore = new TestDataStore(restaurants); + _reviewDataStore = new TestDataStore(reviews); + _userDataStore = new TestDataStore(users); + _reviewApi = new ReviewApi(_restaurantStore, _reviewDataStore, _userDataStore); + } + + [TestMethod] + public void AddRestaurant_ReturnsTrue_WhenRestaurantWithMatchingNameAndAddressDoesNotExist() + { + bool result = _reviewApi.AddRestaurant("Hanks", "123 Main", "City", "PA", "00000", "US"); + Assert.IsTrue(result); + } + + [TestMethod] + public void AddRestaurant_ReturnsFalse_WhenRestaurantWithMatchingNameAndAddressExists() + { + bool result = _reviewApi.AddRestaurant("Bryan's Speakeasy", "205 N Sprague Ave", "Pittsburgh", "PA", "15202", "United States"); + Assert.IsFalse(result); + } + + [TestMethod] + public void GetRestaurants_ReturnsOnlyRestaurantsMatchingAllCriteria() + { + var restaurants = _reviewApi.GetRestaurants("", "Pittsburgh", "", "", "United States").ToList(); + Assert.AreEqual(3, restaurants.Count); + Assert.IsTrue(restaurants.All(item => item.Address.City == "Pittsburgh")); + } + + [TestMethod] + public void AddReview_ReturnsFalse_WhenUserDoesNotExist() + { + var restaurant = _restaurantStore.GetAllItems().First(); + bool result = _reviewApi.AddReview(Guid.NewGuid(), restaurant.Id, 5, 5, 5, 5, ""); + Assert.IsFalse(result); + } + + [TestMethod] + public void AddReview_ReturnsFalse_WhenReviewExistsForUser() + { + var review = _reviewDataStore.GetAllItems().First(); + bool result = _reviewApi.AddReview(review.UserId, review.RestaurantId, 0, 0, 0, 0, ""); + Assert.IsFalse(result); + } + + [TestMethod] + public void AddReview_ReturnsTrue_WhenUsersExistsAndReviewDoesNot() + { + var user = _userDataStore.GetAllItems().First(); + var restaurant = _restaurantStore.GetAllItems().Last(); + bool result = _reviewApi.AddReview(user.Id, restaurant.Id, 3, 3, 3, 5, "Too expensive!"); + Assert.IsTrue(result); + } + + [TestMethod] + public void GetReviews_ReturnsOnlyReviewsMatchingCriteria() + { + var user = _userDataStore.GetAllItems().First(); + var reviews = _reviewApi.GetReviews(user.Id, Guid.Empty).ToList(); + Assert.AreEqual(1, reviews.Count); + + var restaurant = _restaurantStore.GetAllItems().ToList()[1]; + reviews = _reviewApi.GetReviews(Guid.Empty, restaurant.Id).ToList(); + Assert.AreEqual(1, reviews.Count); + + } + + [TestMethod] + public void GetReviews_ReturnsAllReviews_WhenNoInputIsGiven() + { + var reviews = _reviewApi.GetReviews(Guid.Empty, Guid.Empty).ToList(); + Assert.AreEqual(2, reviews.Count); + } + + [TestMethod] + public void DeleteReview_ReturnsTrueAndRemovesItem() + { + var reviews = _reviewApi.GetReviews(Guid.Empty, Guid.Empty).ToList(); + Assert.AreEqual(2, reviews.Count); + + _reviewApi.DeleteReview(reviews[0].Id); + + reviews = _reviewApi.GetReviews(Guid.Empty, Guid.Empty).ToList(); + Assert.AreEqual(1, reviews.Count); + } + } +} diff --git a/SoftWriters.RestaurantReviews.WebApi.Tests/SoftWriters.RestaurantReviews.WebApi.Tests.csproj b/SoftWriters.RestaurantReviews.WebApi.Tests/SoftWriters.RestaurantReviews.WebApi.Tests.csproj new file mode 100644 index 00000000..47856941 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi.Tests/SoftWriters.RestaurantReviews.WebApi.Tests.csproj @@ -0,0 +1,80 @@ + + + + + + Debug + AnyCPU + {B36A5C8D-5C18-47E9-A3D9-070FD70706AD} + Library + Properties + SoftWriters.RestaurantReviews.WebApi.Tests + SoftWriters.RestaurantReviews.WebApi.Tests + v4.6.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8} + SoftWriters.RestaurantReviews.DataLibrary + + + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07} + SoftWriters.RestaurantReviews.WebApi + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/SoftWriters.RestaurantReviews.WebApi.Tests/TestDataStore.cs b/SoftWriters.RestaurantReviews.WebApi.Tests/TestDataStore.cs new file mode 100644 index 00000000..383d74a3 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi.Tests/TestDataStore.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SoftWriters.RestaurantReviews.DataLibrary; + +namespace SoftWriters.RestaurantReviews.WebApi.Tests +{ + public class TestDataStore : IDataStore + { + private readonly List _items; + + public TestDataStore(IEnumerable initialItems) + { + _items = initialItems.ToList(); + } + + public IEnumerable GetAllItems() + { + return _items; + } + + public IEnumerable GetItems(Func predicate) + { + return _items.Where(predicate); + } + + public void AddItem(T item) + { + _items.Add(item); + } + + public void DeleteItem(T item) + { + _items.Remove(item); + } + } +} diff --git a/SoftWriters.RestaurantReviews.WebApi.Tests/packages.config b/SoftWriters.RestaurantReviews.WebApi.Tests/packages.config new file mode 100644 index 00000000..238840db --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SoftWriters.RestaurantReviews.WebApi/Api.cs b/SoftWriters.RestaurantReviews.WebApi/Api.cs new file mode 100644 index 00000000..ed33be81 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi/Api.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.ServiceModel; +using System.ServiceModel.Syndication; +using System.ServiceModel.Web; +using SoftWriters.RestaurantReviews.DataLibrary; + +namespace SoftWriters.RestaurantReviews.WebApi +{ + public class ReviewApi : IReviewApi + { + private readonly IDataStore _restaurantDataStore; + private readonly IDataStore _reviewDataStore; + private readonly IDataStore _userDataStore; + + public ReviewApi() + { + _restaurantDataStore = new XmlDataStore(); + _reviewDataStore = new XmlDataStore(); + _userDataStore = new XmlDataStore(); + } + + public ReviewApi(IDataStore restaurantDataStore, IDataStore reviewDataStore, IDataStore userDataStore) + { + _restaurantDataStore = restaurantDataStore; + _reviewDataStore = reviewDataStore; + _userDataStore = userDataStore; + + } + + public IEnumerable GetRestaurants(string street, string city, string stateCode, string postalCode, string country) + { + var matches = _restaurantDataStore.GetItems(item => + (string.IsNullOrEmpty(street) || item.Address.StreetAddress == street) && + (string.IsNullOrEmpty(city) || item.Address.City == city) && + (string.IsNullOrEmpty(stateCode) || item.Address.StateCode == stateCode) && + (string.IsNullOrEmpty(postalCode) || item.Address.PostalCode == postalCode) && + (string.IsNullOrEmpty(country) || item.Address.Country == country)); + + return matches; + } + + public IEnumerable GetReviews(Guid userId, Guid restaurantId) + { + var matches = _reviewDataStore.GetItems(item => + (userId == Guid.Empty || item.UserId == userId) && + (restaurantId == Guid.Empty || item.RestaurantId == restaurantId)); + + return matches; + } + + public bool AddRestaurant(string name, string street, string city, string stateCode, string postalCode, string country) + { + var matches = _restaurantDataStore.GetItems(item => item.Name == name + && item.Address.StreetAddress == street + && item.Address.City == city + && item.Address.StateCode == stateCode + && item.Address.PostalCode == postalCode + && item.Address.Country == country); + + if (matches.Any()) + return false; + + var address = new Address(street, postalCode, city, stateCode, country); + var id = Guid.NewGuid(); + var restaurant = new Restaurant(id, name, address); + _restaurantDataStore.AddItem(restaurant); + + return true; + } + + public bool AddReview(Guid userId, Guid restaurantId, int overallRating, int foodRating, int serviceRating, + int costRating, string comments) + { + var matchingReviews = _reviewDataStore.GetItems(item => item.UserId == userId && item.RestaurantId == restaurantId); + if (matchingReviews.Any()) + return false; + + var matchingUser = _userDataStore.GetItems(item => item.Id == userId).SingleOrDefault(); + if (matchingUser == null) + return false; + + var id = Guid.NewGuid(); + var review = new Review(id, userId, restaurantId, overallRating, foodRating, serviceRating, costRating, comments); + _reviewDataStore.AddItem(review); + return true; + } + + public bool DeleteReview(Guid id) + { + var review = _reviewDataStore.GetItems(item => item.Id == id).SingleOrDefault(); + if(review != null) + _reviewDataStore.DeleteItem(review); + + return true; + } + } + + [ServiceContract] + public interface IReviewApi + { + [WebInvoke(UriTemplate = "/GetRestaurants?street={street}&city={city}&stateCode={stateCode}&postalCode={postalCode}&country={country}", Method = "GET", ResponseFormat = WebMessageFormat.Json)] + [OperationContract] + IEnumerable GetRestaurants(string street, string city, string stateCode, string postalCode, string country); + + [WebInvoke(UriTemplate = "/GetReviews?userId={userId}&restaurantId={restaurantId}", Method = "GET", ResponseFormat = WebMessageFormat.Json)] + [OperationContract] + IEnumerable GetReviews(Guid userId, Guid restaurantId); + + [WebInvoke(UriTemplate = "/AddRestaurant?name={name}&street={street}&city={city}&stateCode={stateCode}&postalCode={postalCode}&country={country}", Method = "POST", ResponseFormat = WebMessageFormat.Json)] + [OperationContract] + bool AddRestaurant(string name, string street, string city, string stateCode, string postalCode, string country); + + [WebInvoke(UriTemplate = "/AddReview?userId={userId}&restaurantId={restaurantId}&overallRating={overallRating}&foodRating={foodRating}&serviceRating={serviceRating}&costRating={costRating}&comments={comments}", Method = "POST", ResponseFormat = WebMessageFormat.Json)] + [OperationContract] + bool AddReview(Guid userId, Guid restaurantId, int overallRating, int foodRating, int serviceRating, int costRating, string comments); + + [WebInvoke(UriTemplate = "/DeleteReview?id={id}", Method = "DELETE", ResponseFormat = WebMessageFormat.Json)] + [OperationContract] + bool DeleteReview(Guid id); + } +} diff --git a/SoftWriters.RestaurantReviews.WebApi/Properties/AssemblyInfo.cs b/SoftWriters.RestaurantReviews.WebApi/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..cd5a59de --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SoftWriters.RestaurantReviews.WebApi")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SoftWriters.RestaurantReviews.WebApi")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4f38eb50-bb3a-44e7-9fe2-3600fcc0be07")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SoftWriters.RestaurantReviews.WebApi/SoftWriterException.cs b/SoftWriters.RestaurantReviews.WebApi/SoftWriterException.cs new file mode 100644 index 00000000..4b147988 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi/SoftWriterException.cs @@ -0,0 +1,16 @@ +using System; + +namespace SoftWriters.RestaurantReviews.WebApi +{ + public class SoftWriterException : Exception + { + public SoftWriterException() + { } + + public SoftWriterException(string message) : base(message) + { } + + public SoftWriterException(string message, Exception innerException) : base(message, innerException) + { } + } +} diff --git a/SoftWriters.RestaurantReviews.WebApi/SoftWriters.RestaurantReviews.WebApi.csproj b/SoftWriters.RestaurantReviews.WebApi/SoftWriters.RestaurantReviews.WebApi.csproj new file mode 100644 index 00000000..b2a37b03 --- /dev/null +++ b/SoftWriters.RestaurantReviews.WebApi/SoftWriters.RestaurantReviews.WebApi.csproj @@ -0,0 +1,58 @@ + + + + + Debug + AnyCPU + {4F38EB50-BB3A-44E7-9FE2-3600FCC0BE07} + Library + Properties + SoftWriters.RestaurantReviews.WebApi + SoftWriters.RestaurantReviews.WebApi + v4.6.1 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {F2472982-DC7E-4EB8-86DD-45F1E3D189E8} + SoftWriters.RestaurantReviews.DataLibrary + + + + \ No newline at end of file