diff --git a/.gitignore b/.gitignore index 40b878d..cb22fd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -node_modules/ \ No newline at end of file +node_modules/ + +# Ignore .NET build outputs +bin/ +obj/ +**/bin/ +**/obj/ \ No newline at end of file diff --git a/code/.gitignore b/code/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/code/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/code/CommBank.Tests/CommBank.Tests.csproj b/code/CommBank.Tests/CommBank.Tests.csproj new file mode 100644 index 0000000..8673505 --- /dev/null +++ b/code/CommBank.Tests/CommBank.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/code/CommBank.Tests/Fakes/FakeGoalsService.cs b/code/CommBank.Tests/Fakes/FakeGoalsService.cs new file mode 100644 index 0000000..b037e17 --- /dev/null +++ b/code/CommBank.Tests/Fakes/FakeGoalsService.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommBank.Models; +using CommBank.Services; + +namespace CommBank.Tests.Fakes; + +public class FakeGoalsService : IGoalsService +{ + private readonly List _goals; + + public FakeGoalsService(List goals) + { + _goals = goals; + } + + public Task> GetAsync() => + Task.FromResult(_goals); + + public Task GetAsync(string id) => + Task.FromResult(_goals.FirstOrDefault(g => g.Id == id)); + + public Task CreateAsync(Goal newGoal) + { + _goals.Add(newGoal); + return Task.CompletedTask; + } + + public Task UpdateAsync(string id, Goal updatedGoal) + { + var idx = _goals.FindIndex(g => g.Id == id); + if (idx >= 0) _goals[idx] = updatedGoal; + return Task.CompletedTask; + } + + public Task RemoveAsync(string id) + { + _goals.RemoveAll(g => g.Id == id); + return Task.CompletedTask; + } + + public Task> GetForUserAsync(string userId) => + Task.FromResult(_goals.Where(g => g.UserId == userId).ToList()); +} diff --git a/code/CommBank.Tests/GoalControllerTests.cs b/code/CommBank.Tests/GoalControllerTests.cs new file mode 100644 index 0000000..258bc6f --- /dev/null +++ b/code/CommBank.Tests/GoalControllerTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using CommBank.Controllers; +using CommBank.Models; +using CommBank.Tests.Fakes; + + +public class GoalControllerTests +{ + [Fact] + public async Task GetForUser_ReturnsOnlyGoalsForThatUser() + { + // Arrange + var userId = "62a3f5e0102e921da1253d33"; + + var goals = new List + { + new Goal { Id = "62a3f5e0102e921da1253d32", UserId = userId, Name = "G1" }, + new Goal { Id = "62a3f5e0102e921da1253d34", UserId = userId, Name = "G2" }, + new Goal { Id = "62a3f5e0102e921da1253d99", UserId = "62a3f5e0102e921da1253d00", Name = "Other user" }, + }; + + var goalsService = new FakeGoalsService(goals); + var controller = new GoalController(goalsService); + + // Act + var result = await controller.GetForUser(userId); + + // Assert + Assert.NotNull(result); + + foreach (var goal in result) + { + Assert.IsAssignableFrom(goal); + Assert.Equal(userId, goal.UserId); + } + } +} diff --git a/code/CommBank/CommBank.csproj b/code/CommBank/CommBank.csproj new file mode 100644 index 0000000..a9a8042 --- /dev/null +++ b/code/CommBank/CommBank.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + + diff --git a/code/CommBank/CommBank.http b/code/CommBank/CommBank.http new file mode 100644 index 0000000..2224628 --- /dev/null +++ b/code/CommBank/CommBank.http @@ -0,0 +1,6 @@ +@CommBank_HostAddress = http://localhost:5117 + +GET {{CommBank_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/code/CommBank/Controllers/GoalController.cs b/code/CommBank/Controllers/GoalController.cs new file mode 100644 index 0000000..745995b --- /dev/null +++ b/code/CommBank/Controllers/GoalController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc; +using CommBank.Models; +using CommBank.Services; + +namespace CommBank.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class GoalController : ControllerBase +{ + private readonly IGoalsService _goalsService; + + public GoalController(IGoalsService goalsService) => + _goalsService = goalsService; + + [HttpGet] + public async Task> Get() => + await _goalsService.GetAsync(); + + [HttpGet("{id:length(24)}")] + public async Task> Get(string id) + { + var goal = await _goalsService.GetAsync(id); + if (goal is null) + { + return NotFound(); + } + return goal; + + } + + [HttpGet("user/{userId:length(24)}")] + public async Task> GetForUser(string userId) => + await _goalsService.GetForUserAsync(userId); + + + [HttpPost] + public async Task Post(Goal newGoal) + { + await _goalsService.CreateAsync(newGoal); + return CreatedAtAction(nameof(Get), new{ id = newGoal.Id}, newGoal); + + } + + [HttpPut("{id:length(24)}")] + public async Task Update(string id, Goal updatedGoal) + { + var goal = await _goalsService.GetAsync(id); + if (goal is null) + { + return NotFound(); + } + + updatedGoal.Id = goal.Id; + + await _goalsService.UpdateAsync(id, updatedGoal); + return NoContent(); + } + + [HttpDelete("{id:length(24)}")] + public async Task Delete(string id) + { + var goal = await _goalsService.GetAsync(id); + + if(goal is null) + { + return NotFound(); + } + + await _goalsService.RemoveAsync(id); + return NoContent(); + } + + +} \ No newline at end of file diff --git a/code/CommBank/Models/Goal.cs b/code/CommBank/Models/Goal.cs new file mode 100644 index 0000000..c8b1850 --- /dev/null +++ b/code/CommBank/Models/Goal.cs @@ -0,0 +1,35 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace CommBank.Models; + +public class Goal +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + public string? Name { get; set; } + + public UInt64 TargetAmount { get; set; } = 0; + + public DateTime TargetDate { get; set; } + + public double Balance { get; set; } = 0.00; + + public DateTime Created { get; set; } = DateTime.Now; + + [BsonRepresentation(BsonType.ObjectId)] + public string? AccountId { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public List? TransactionIds { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public List? TagIds { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public string? UserId { get; set; } + + public string? Icon { get; set; } +} \ No newline at end of file diff --git a/code/CommBank/Program.cs b/code/CommBank/Program.cs new file mode 100644 index 0000000..e1105fa --- /dev/null +++ b/code/CommBank/Program.cs @@ -0,0 +1,37 @@ +using CommBank.Services; +using MongoDB.Driver; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("Secrets.json"); + +// mongo + db instance +var MongoClient = new MongoClient(builder.Configuration.GetConnectionString("Commbank")); +var mongoDatabase = MongoClient.GetDatabase("data"); + +// test connection to db +try +{ + await mongoDatabase.RunCommandAsync((Command)"{ping:1}"); + Console.WriteLine("✅ MongoDB connection OK (ping succeeded)"); +} +catch (Exception ex) +{ + Console.WriteLine("❌ MongoDB connection failed: " + ex.Message); +} + + +// register dependencies for DI +IGoalsService goalsService = new GoalsService(mongoDatabase); +builder.Services.AddSingleton(goalsService); + +// enable controoller to handle routes +builder.Services.AddControllers(); + +var app = builder.Build(); + +// map routes +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/code/CommBank/Properties/launchSettings.json b/code/CommBank/Properties/launchSettings.json new file mode 100644 index 0000000..7864a12 --- /dev/null +++ b/code/CommBank/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:13994", + "sslPort": 44316 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5117", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7298;http://localhost:5117", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/code/CommBank/Secrets.json b/code/CommBank/Secrets.json new file mode 100644 index 0000000..1fafaa5 --- /dev/null +++ b/code/CommBank/Secrets.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "CommBank": "mongodb+srv://user1:fYvb7tH3TQaKuPtd@cbafencer.y1186pi.mongodb.net/?appName=cbaFencer" + } +} \ No newline at end of file diff --git a/code/CommBank/Services/GoalsService.cs b/code/CommBank/Services/GoalsService.cs new file mode 100644 index 0000000..365f977 --- /dev/null +++ b/code/CommBank/Services/GoalsService.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Options; +using CommBank.Models; +using MongoDB.Driver; + +namespace CommBank.Services; + +public class GoalsService : IGoalsService +{ + private readonly IMongoCollection _goalsCollection; + + public GoalsService(IMongoDatabase mongoDatabase) + { + _goalsCollection = mongoDatabase.GetCollection("Goals"); + + } + public async Task> GetAsync() => + await _goalsCollection.Find(_ => true).ToListAsync(); + + public async Task GetAsync(string id) => + await _goalsCollection.Find(x => x.Id == id).FirstOrDefaultAsync(); + + public async Task CreateAsync(Goal newGoal) => + await _goalsCollection.InsertOneAsync(newGoal); + + public async Task UpdateAsync(string id, Goal updatedGoal) => + await _goalsCollection.ReplaceOneAsync(x => x.Id == id, updatedGoal); + + public async Task RemoveAsync(string id) => + await _goalsCollection.DeleteOneAsync(x => x.Id == id); + + public async Task> GetForUserAsync(string userId) => + await _goalsCollection.Find(g => g.UserId == userId).ToListAsync(); + +} diff --git a/code/CommBank/Services/IGoalsService.cs b/code/CommBank/Services/IGoalsService.cs new file mode 100644 index 0000000..843e2ef --- /dev/null +++ b/code/CommBank/Services/IGoalsService.cs @@ -0,0 +1,15 @@ +using CommBank.Models; + +namespace CommBank.Services +{ + public interface IGoalsService + { + Task CreateAsync(Goal newGoal); + Task> GetAsync(); + Task GetAsync(string id); + Task RemoveAsync(string id); + Task UpdateAsync(string id, Goal updatedGoal); + + Task> GetForUserAsync(string userId); + } +} \ No newline at end of file diff --git a/code/CommBank/ans.cs b/code/CommBank/ans.cs new file mode 100644 index 0000000..a6c0eb6 --- /dev/null +++ b/code/CommBank/ans.cs @@ -0,0 +1,13 @@ +// { +// "id": "62a3f587102e921da1253d32", +// "name": "House Down Payment", +// "targetAmount": 100000, +// "targetDate": "2025-01-08T05:00:00Z", +// "balance": 73501.82, +// "created": "2022-06-11T01:53:10.857Z", +// "accountId": null, +// "transactionIds": null, +// "tagIds": null, +// "userId": "62a29c15f4605c4c9fa7f306", +// "icon": "🤺" +// } \ No newline at end of file diff --git a/code/CommBank/appsettings.Development.json b/code/CommBank/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/code/CommBank/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/code/CommBank/appsettings.json b/code/CommBank/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/code/CommBank/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/code/Task1.zip b/code/Task1.zip new file mode 100644 index 0000000..f17a2bd Binary files /dev/null and b/code/Task1.zip differ diff --git a/code/task1_ans.txt b/code/task1_ans.txt new file mode 100644 index 0000000..5838d06 --- /dev/null +++ b/code/task1_ans.txt @@ -0,0 +1,13 @@ +{ + "id": "62a3f587102e921da1253d32", + "name": "House Down Payment", + "targetAmount": 100000, + "targetDate": "2025-01-08T05:00:00Z", + "balance": 73501.82, + "created": "2022-06-11T01:53:10.857Z", + "accountId": null, + "transactionIds": null, + "tagIds": null, + "userId": "62a29c15f4605c4c9fa7f306", + "icon": "🤺" +} \ No newline at end of file