added UserController, UserDTO, CreateUser migration, User model

This commit is contained in:
wt committed 2026-06-10 15:34:39 +07:00
1 parent 6357258f45
commit 56d1988b17
13 files changed
+297 -159

No files matched your search

+1
View File
@@ -10,3 +10,4 @@ db.sqlite
.env.*
!.env.example
default.profraw
.vscode
Whitespace-only changes.
@@ -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
}
}
@@ -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
}
}
-17
View File
@@ -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
}
}
+37
View File
@@ -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
}
}
@@ -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()
}
}
@@ -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()
}
}
-29
View File
@@ -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
)
}
}
+49
View File
@@ -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
)
}
}
+3 -3
View File
@@ -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)
+1 -9
View File
@@ -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())
}
+126 -50
View File
@@ -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
}
}