diff --git a/.gitignore b/.gitignore index 745c700..022b057 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ db.sqlite .env.* !.env.example default.profraw +.vscode diff --git a/Sources/auth_service/Controllers/.gitkeep b/Sources/auth_service/Controllers/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/Sources/auth_service/Controllers/TodoController.swift b/Sources/auth_service/Controllers/TodoController.swift deleted file mode 100644 index e1e9204..0000000 --- a/Sources/auth_service/Controllers/TodoController.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Fluent -import Vapor - -struct TodoController: RouteCollection { - func boot(routes: any RoutesBuilder) throws { - let todos = routes.grouped("todos") - - todos.get(use: self.index) - todos.post(use: self.create) - todos.group(":todoID") { todo in - todo.delete(use: self.delete) - } - } - - @Sendable - func index(req: Request) async throws -> [TodoDTO] { - try await Todo.query(on: req.db).all().map { $0.toDTO() } - } - - @Sendable - func create(req: Request) async throws -> TodoDTO { - let todo = try req.content.decode(TodoDTO.self).toModel() - - try await todo.save(on: req.db) - return todo.toDTO() - } - - @Sendable - func delete(req: Request) async throws -> HTTPStatus { - guard let todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else { - throw Abort(.notFound) - } - - try await todo.delete(on: req.db) - return .noContent - } -} diff --git a/Sources/auth_service/Controllers/UserController.swift b/Sources/auth_service/Controllers/UserController.swift new file mode 100644 index 0000000..1e0fe51 --- /dev/null +++ b/Sources/auth_service/Controllers/UserController.swift @@ -0,0 +1,62 @@ +import Fluent +import Vapor + +struct UserController: RouteCollection { + func boot(routes: any RoutesBuilder) throws { + let users = routes.grouped("users") + + users.get(use: self.index) + users.post(use: self.create) + users.group(":userID") { user in + user.get(use: show) + user.put(use: update) + user.delete(use: self.delete) + } + } + + @Sendable + func index(req: Request) async throws -> [UserDTO] { + try await User.query(on: req.db).all().map { $0.toDTO() } + } + + @Sendable + func create(req: Request) async throws -> UserDTO { + let user = try req.content.decode(UserDTO.self).toModel() + + try await user.save(on: req.db) + return user.toDTO() + } + + @Sendable + func show(req: Request) async throws -> UserDTO { + guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else { + throw Abort(.notFound) + } + return user.toDTO() + } + + @Sendable + func update(req: Request) async throws -> UserDTO { + guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else { + throw Abort(.notFound) + } + let updatedUser = try req.content.decode(User.self) + user.username = updatedUser.username + user.email = updatedUser.email + user.password = updatedUser.password + user.group = updatedUser.group + user.isSuperuser = updatedUser.isSuperuser + try await user.save(on: req.db) + return user.toDTO() + } + + @Sendable + func delete(req: Request) async throws -> HTTPStatus { + guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else { + throw Abort(.notFound) + } + + try await user.delete(on: req.db) + return .noContent + } +} diff --git a/Sources/auth_service/DTOs/TodoDTO.swift b/Sources/auth_service/DTOs/TodoDTO.swift deleted file mode 100644 index c35ba33..0000000 --- a/Sources/auth_service/DTOs/TodoDTO.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Fluent -import Vapor - -struct TodoDTO: Content { - var id: UUID? - var title: String? - - func toModel() -> Todo { - let model = Todo() - - model.id = self.id - if let title = self.title { - model.title = title - } - return model - } -} diff --git a/Sources/auth_service/DTOs/UserDTO.swift b/Sources/auth_service/DTOs/UserDTO.swift new file mode 100644 index 0000000..903e7b3 --- /dev/null +++ b/Sources/auth_service/DTOs/UserDTO.swift @@ -0,0 +1,37 @@ +import Fluent +import Vapor + +struct UserDTO: Content { + var id: UUID? + var username: String? + var email: String? + var password: String? + var group: String? + var isSuperuser: Bool? + + func toModel() -> User { + let model = User() + + model.id = self.id + if let username = self.username { + model.username = username + } + if let email = self.email { + model.email = email + } + if let password = self.password { + model.password = password + } + if let group = self.group { + model.group = group + } else { + model.group = "users" + } + if let isSuperuser = self.isSuperuser { + model.isSuperuser = isSuperuser + } else { + model.isSuperuser = false + } + return model + } +} diff --git a/Sources/auth_service/Migrations/CreateTodo.swift b/Sources/auth_service/Migrations/CreateTodo.swift deleted file mode 100644 index 7466919..0000000 --- a/Sources/auth_service/Migrations/CreateTodo.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Fluent - -struct CreateTodo: AsyncMigration { - func prepare(on database: any Database) async throws { - try await database.schema("todos") - .id() - .field("title", .string, .required) - .create() - } - - func revert(on database: any Database) async throws { - try await database.schema("todos").delete() - } -} diff --git a/Sources/auth_service/Migrations/CreateUser.swift b/Sources/auth_service/Migrations/CreateUser.swift new file mode 100644 index 0000000..666ec67 --- /dev/null +++ b/Sources/auth_service/Migrations/CreateUser.swift @@ -0,0 +1,18 @@ +import Fluent + +struct CreateUser: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema("users") + .id() + .field("username", .string, .required) + .field("email", .string, .required) + .field("password", .string, .required) + .field("group", .string, .required) + .field("is_superuser", .bool, .required) + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema("users").delete() + } +} diff --git a/Sources/auth_service/Models/Todo.swift b/Sources/auth_service/Models/Todo.swift deleted file mode 100644 index de1837d..0000000 --- a/Sources/auth_service/Models/Todo.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Fluent -import struct Foundation.UUID - -/// Property wrappers interact poorly with `Sendable` checking, causing a warning for the `@ID` property -/// It is recommended you write your model with sendability checking on and then suppress the warning -/// afterwards with `@unchecked Sendable`. -final class Todo: Model, @unchecked Sendable { - static let schema = "todos" - - @ID(key: .id) - var id: UUID? - - @Field(key: "title") - var title: String - - init() { } - - init(id: UUID? = nil, title: String) { - self.id = id - self.title = title - } - - func toDTO() -> TodoDTO { - .init( - id: self.id, - title: self.$title.value - ) - } -} diff --git a/Sources/auth_service/Models/User.swift b/Sources/auth_service/Models/User.swift new file mode 100644 index 0000000..d96c51f --- /dev/null +++ b/Sources/auth_service/Models/User.swift @@ -0,0 +1,49 @@ +import Fluent + +import struct Foundation.UUID + +final class User: Model, @unchecked Sendable { + static let schema = "users" + + @ID(key: .id) + var id: UUID? + + @Field(key: "username") + var username: String + + @Field(key: "email") + var email: String + + @Field(key: "password") + var password: String + + @Field(key: "group") + var group: String + + @Field(key: "is_superuser") + var isSuperuser: Bool + + init() {} + + init( + id: UUID? = nil, username: String, email: String, password: String, group: String, + isSuperuser: Bool + ) { + self.id = id + self.username = username + self.email = email + self.password = password + self.group = group + self.isSuperuser = isSuperuser + } + + func toDTO() -> UserDTO { + .init( + id: self.id, + username: self.username, + email: self.email, + group: self.group, + isSuperuser: self.isSuperuser + ) + } +} diff --git a/Sources/auth_service/configure.swift b/Sources/auth_service/configure.swift index 89b9bf8..370aed4 100644 --- a/Sources/auth_service/configure.swift +++ b/Sources/auth_service/configure.swift @@ -1,6 +1,6 @@ -import NIOSSL import Fluent import FluentSQLiteDriver +import NIOSSL import Vapor // configures your application @@ -10,8 +10,8 @@ public func configure(_ app: Application) async throws { app.databases.use(DatabaseConfigurationFactory.sqlite(.file("db.sqlite")), as: .sqlite) - app.migrations.add(CreateTodo()) - app.http.server.configuration.address = .hostname("localhost", port: 9090) + app.migrations.add(CreateUser()) + app.http.server.configuration.address = .hostname("localhost", port: 43243) // register routes try routes(app) diff --git a/Sources/auth_service/routes.swift b/Sources/auth_service/routes.swift index f958df1..7229e5c 100644 --- a/Sources/auth_service/routes.swift +++ b/Sources/auth_service/routes.swift @@ -2,13 +2,5 @@ import Fluent import Vapor func routes(_ app: Application) throws { - app.get { req async in - "It works!" - } - - app.get("hello") { req async -> String in - "Hello, world!" - } - - try app.register(collection: TodoController()) + try app.register(collection: UserController()) } diff --git a/Tests/auth_serviceTests/auth_serviceTests.swift b/Tests/auth_serviceTests/auth_serviceTests.swift index 056218b..8429416 100644 --- a/Tests/auth_serviceTests/auth_serviceTests.swift +++ b/Tests/auth_serviceTests/auth_serviceTests.swift @@ -1,11 +1,12 @@ -@testable import auth_service -import VaporTesting -import Testing import Fluent +import Testing +import VaporTesting + +@testable import auth_service @Suite("App Tests with DB", .serialized) struct auth_serviceTests { - private func withApp(_ test: (Application) async throws -> ()) async throws { + private func withApp(_ test: (Application) async throws -> Void) async throws { let app = try await Application.make(.testing) do { try await configure(app) @@ -19,66 +20,141 @@ struct auth_serviceTests { } try await app.asyncShutdown() } - - @Test("Test Hello World Route") - func helloWorld() async throws { + + @Test("Getting all the Users") + func getAllUsers() async throws { try await withApp { app in - try await app.testing().test(.GET, "hello", afterResponse: { res async in - #expect(res.status == .ok) - #expect(res.body.string == "Hello, world!") - }) + let sampleUsers = [ + User( + username: "user1", email: "user1@example.com", password: "user1password", + group: "users", isSuperuser: true), + User( + username: "user2", email: "user2@example.com", password: "user2password", + group: "users", isSuperuser: false), + ] + try await sampleUsers.create(on: app.db) + + try await app.testing().test( + .GET, "users", + afterResponse: { res async throws in + #expect(res.status == .ok) + #expect( + try res.content.decode([UserDTO].self).sorted(by: { + ($0.username ?? "") < ($1.username ?? "") + }) + == sampleUsers.map { $0.toDTO() }.sorted(by: { + ($0.username ?? "") < ($1.username ?? "") + }) + ) + }) } } - - @Test("Getting all the Todos") - func getAllTodos() async throws { + + @Test("Creating a User") + func createUser() async throws { + let newDTO = UserDTO( + id: nil, username: "test", email: "test@example.com", password: "testpassword") + try await withApp { app in - let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")] - try await sampleTodos.create(on: app.db) - - try await app.testing().test(.GET, "todos", afterResponse: { res async throws in - #expect(res.status == .ok) - #expect(try - res.content.decode([TodoDTO].self).sorted(by: { ($0.title ?? "") < ($1.title ?? "") }) == - sampleTodos.map { $0.toDTO() }.sorted(by: { ($0.title ?? "") < ($1.title ?? "") }) - ) - }) + try await app.testing().test( + .POST, "users", + beforeRequest: { req in + try req.content.encode(newDTO) + }, + afterResponse: { res async throws in + #expect(res.status == .ok) + let models = try await User.query(on: app.db).all() + #expect(models.map({ $0.toDTO().username }) == [newDTO.username]) + }) } } - - @Test("Creating a Todo") - func createTodo() async throws { - let newDTO = TodoDTO(id: nil, title: "test") - + + @Test("Deleting a User") + func deleteUser() async throws { + let testUsers = [ + User( + username: "user1", email: "user1@example.com", password: "user1password", + group: "users", isSuperuser: true), + User( + username: "user2", email: "user2@example.com", password: "user2password", + group: "users", isSuperuser: false), + ] + try await withApp { app in - try await app.testing().test(.POST, "todos", beforeRequest: { req in - try req.content.encode(newDTO) - }, afterResponse: { res async throws in - #expect(res.status == .ok) - let models = try await Todo.query(on: app.db).all() - #expect(models.map({ $0.toDTO().title }) == [newDTO.title]) - }) + try await testUsers.create(on: app.db) + + try await app.testing().test( + .DELETE, "users/\(testUsers[0].requireID())", + afterResponse: { res async throws in + #expect(res.status == .noContent) + let model = try await User.find(testUsers[0].id, on: app.db) + #expect(model == nil) + }) } } - - @Test("Deleting a Todo") - func deleteTodo() async throws { - let testTodos = [Todo(title: "test1"), Todo(title: "test2")] - + + @Test("Getting a User by ID") + func showUser() async throws { + let testUsers = [ + User( + username: "user1", email: "user1@example.com", password: "user1password", + group: "users", isSuperuser: true), + User( + username: "user2", email: "user2@example.com", password: "user2password", + group: "users", isSuperuser: false), + ] try await withApp { app in - try await testTodos.create(on: app.db) - - try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in - #expect(res.status == .noContent) - let model = try await Todo.find(testTodos[0].id, on: app.db) - #expect(model == nil) - }) + try await testUsers.create(on: app.db) + + try await app.testing().test( + .GET, "users/\(testUsers[0].requireID())", + afterResponse: { res async throws in + #expect(res.status == .ok) + let user = try res.content.decode(UserDTO.self) + #expect(user.id == testUsers[0].id) + #expect(user.username == testUsers[0].username) + }) + } + } + + @Test("Updating a User") + func updateUser() async throws { + let testUsersDTOs = [ + UserDTO( + username: "user1", email: "user1@example.com", password: "user1password", + group: "users", isSuperuser: true), + UserDTO( + username: "user2", email: "user2@example.com", password: "user2password", + group: "users", isSuperuser: false), + ] + let testUsers = [ + User( + username: "user1", email: "user1@example.com", password: "user1password", + group: "users", isSuperuser: true), + User( + username: "user2", email: "user2@example.com", password: "user2password", + group: "users", isSuperuser: false), + ] + try await withApp { app in + try await testUsers.create(on: app.db) + + try await app.testing().test( + .PUT, "users/\(testUsers[0].requireID())", + beforeRequest: { req in + try req.content.encode(testUsersDTOs[0]) + }, + afterResponse: { res async throws in + #expect(res.status == .ok) + let user = try res.content.decode(UserDTO.self) + #expect(user.id == testUsers[0].id) + #expect(user.username == testUsers[0].username) + }) } } } -extension TodoDTO: Equatable { +extension UserDTO: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.id == rhs.id && lhs.title == rhs.title + lhs.id == rhs.id && lhs.username == rhs.username } }