This repository was archived by the owner on Sep 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from multipletwigs/feature/POST-human-user
feature: Human User Creation Endpoint
- Loading branch information
Showing
9 changed files
with
279 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ import { | |
createApiToken, | ||
createSuperAdmin, | ||
} from "@/core/application/controllers/userController"; | ||
import { createHumanUser } from "@/core/application/services/userService"; | ||
import { ulid } from "ulid"; | ||
|
||
export async function createSuperAdminForTesting(): Promise<string | null> { | ||
const userData = { | ||
|
@@ -20,3 +22,22 @@ export async function createSuperAdminForTesting(): Promise<string | null> { | |
|
||
return apiKey; | ||
} | ||
|
||
export async function createHumanUserForTesting(): Promise<string | null> { | ||
const userData = { | ||
name: "Sprout Tester", | ||
email: "[email protected]" | ||
}; | ||
|
||
const humanUserId = await createHumanUser(userData); | ||
if (!humanUserId) { | ||
throw new Error("Failed to create human user"); | ||
} | ||
|
||
const apiKey = await createApiToken(humanUserId); | ||
if (!apiKey) { | ||
throw new Error("Failed to create API key for human user"); | ||
} | ||
|
||
return apiKey; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export const UNAUTHORIZED_INVALID_TOKEN = { | ||
message: "Unauthorized: Invalid token", | ||
code: 401, | ||
} | ||
|
||
export const UNAUTHORIZED_NO_PERMISSION_CREATE = { | ||
message: "Unauthorized: User does not have permission to create users", | ||
code: 403, | ||
}; | ||
|
||
export const UNAUTHORIZED_NO_PERMISSION_DELETE = { | ||
message: "Unauthorized: User does not have permission to delete users", | ||
code: 403, | ||
} | ||
|
||
export const USER_CREATED_SUCCESSFULLY = { | ||
message: "User created successfully", | ||
code: 201, | ||
} | ||
|
||
export const UNSUCCESSFUL_USER_CREATION = { | ||
message: "User creation not successful. Something went wrong.", | ||
code: 500, | ||
} |
133 changes: 133 additions & 0 deletions
133
src/core/application/controllers/user/userController.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import { createHumanUserForTesting, createSuperAdminForTesting } from "@/__tests__/utils"; | ||
import { app } from "@/index"; | ||
import { test, expect, describe, beforeAll } from "bun:test"; | ||
import { getUser } from "../../services/userService"; | ||
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues"; | ||
import { UNAUTHORIZED_NO_PERMISSION_CREATE } from "./returnValues"; | ||
import { getThread } from "../../services/threadService"; | ||
import { parseToken } from "../../services/tokenService"; | ||
import { Thread } from "@/core/domain/thread"; | ||
|
||
describe.only("userController", async () => { | ||
let superAdminToken: string | null; | ||
let humanUserToken: string | null; | ||
|
||
beforeAll(async () => { | ||
superAdminToken = await createSuperAdminForTesting(); | ||
humanUserToken = await createHumanUserForTesting(); | ||
}); | ||
|
||
test("allows creating a user and the user is saved in the database", async () => { | ||
const request = new Request("http://localhost:8080/users", { | ||
headers: { | ||
authorization: `Bearer ${superAdminToken}`, | ||
"Content-Type": "application/json" | ||
}, | ||
method: "POST", | ||
body: JSON.stringify({ | ||
name: "Mr. Sprout", | ||
email: "[email protected]" | ||
}) | ||
}); | ||
|
||
const response = await app.handle(request); | ||
const responseJson: any = await response.json(); | ||
|
||
expect(responseJson).toHaveProperty("token"); | ||
const apiToken = responseJson.token; | ||
expect(apiToken).not.toBeNull(); | ||
|
||
// From api token we can get the user id | ||
const parsedToken = await parseToken(apiToken); | ||
|
||
if (parsedToken) { | ||
const user = await getUser(parsedToken.userId); | ||
expect(user).not.toBeNull(); | ||
expect(user?.name).toEqual("Mr. Sprout"); | ||
expect(user?.email).toEqual("[email protected]"); | ||
} | ||
}); | ||
|
||
test("prevents from creating a user if there is no api token present", async () => { | ||
const request = new Request("http://localhost:8080/users", { | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
method: "POST", | ||
body: JSON.stringify({ | ||
name: "Mr. Sprout", | ||
email: "[email protected]" | ||
}) | ||
}); | ||
|
||
const response: any = await app.handle(request).then((response) => response.json()) | ||
expect(response.message).toBe(UNAUTHORIZED_MISSING_TOKEN.message); | ||
}); | ||
|
||
test("prevents from creating a user if the user does not have CREATE_USER permission", async () => { | ||
const request = new Request("http://localhost:8080/users", { | ||
headers: { | ||
authorization: `Bearer ${humanUserToken}`, | ||
"Content-Type": "application/json" | ||
}, | ||
method: "POST", | ||
body: JSON.stringify({ | ||
name: "Jr. Sprout", | ||
email: "[email protected]" | ||
}) | ||
}); | ||
|
||
const response: any = await app.handle(request); | ||
const responseJson = await response.json(); | ||
expect(responseJson.message).toBe(UNAUTHORIZED_NO_PERMISSION_CREATE.message); | ||
}); | ||
|
||
test("allows a human user to create a thread", async () => { | ||
const request = new Request("http://localhost:8080/thread", { | ||
headers: { | ||
authorization: `Bearer ${humanUserToken}`, | ||
"Content-type": "application/json", | ||
}, | ||
method: "POST", | ||
}); | ||
|
||
const response = await app.handle(request); | ||
const responseJson: any = await response.json(); | ||
|
||
expect(responseJson).toHaveProperty("id"); | ||
|
||
const id = responseJson.id; | ||
|
||
const thread = await getThread(id); | ||
|
||
expect(thread).not.toBeNull(); | ||
expect(thread?.id).toEqual(id); | ||
}) | ||
|
||
test("allows a human user to add messages to a thread they created", async () => { | ||
// Creating a thread for the test | ||
const createThreadResponse = await app.handle(new Request("http://localhost:8080/thread", { | ||
headers: { | ||
authorization: `Bearer ${humanUserToken}`, | ||
"Content-type": "application/json", | ||
}, | ||
method: "POST", | ||
})); | ||
const thread: Thread = await createThreadResponse.json() as Thread; | ||
expect(thread).toHaveProperty("id"); | ||
|
||
// Adding a message to the thread | ||
const response = await app.handle(new Request(`http://localhost:8080/thread/${thread.id}/message`, { | ||
headers: { | ||
authorization: `Bearer ${humanUserToken}`, | ||
"Content-type": "application/json", | ||
}, | ||
method: "POST", | ||
body: JSON.stringify({ message: "Human user from userController test!" }) | ||
})); | ||
const messageResponse = await response.json() as any; | ||
|
||
expect(messageResponse).toHaveProperty("content"); | ||
expect(messageResponse.content).toBe("Human user from userController test!"); | ||
}); | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import Elysia, { t } from "elysia"; | ||
import { UNAUTHORIZED_MISSING_TOKEN } from "../../ports/returnValues"; | ||
import { createToken, getTokenPermissions } from "@/core/application/services/tokenService"; | ||
import { UNAUTHORIZED_INVALID_TOKEN } from "../thread/returnValues"; | ||
import { UNAUTHORIZED_NO_PERMISSION_CREATE, UNSUCCESSFUL_USER_CREATION, USER_CREATED_SUCCESSFULLY } from "./returnValues"; | ||
import { createHumanUser } from "../../services/userService"; | ||
|
||
type UserDecorator = { | ||
request: { | ||
bearer: string | undefined; | ||
}; | ||
store: {}; | ||
derive: {}; | ||
resolve: {}; | ||
}; | ||
|
||
export const users = new Elysia<"/users", UserDecorator>(); | ||
|
||
/** | ||
* Guard for the user controller. | ||
* @param {string | undefined} bearer - The bearer token. | ||
* @returns {Promise<Error | undefined>} - Returns an error if the token is missing, invalid or does not have CREATE_USER. | ||
*/ | ||
users.guard({ | ||
beforeHandle: [async ({ bearer, set }) => { | ||
if (!bearer) { | ||
set.status = UNAUTHORIZED_MISSING_TOKEN.code; | ||
return UNAUTHORIZED_MISSING_TOKEN; | ||
} | ||
|
||
// Check that the token is valid and has the required permission. | ||
const permissions = await getTokenPermissions(bearer); | ||
if (!permissions) { | ||
set.status = UNAUTHORIZED_INVALID_TOKEN.code; | ||
return UNAUTHORIZED_INVALID_TOKEN; | ||
} | ||
|
||
if (!permissions.some(p => ["create_user", "*"].includes(p.key))) { | ||
set.status = UNAUTHORIZED_NO_PERMISSION_CREATE.code; | ||
return UNAUTHORIZED_NO_PERMISSION_CREATE; | ||
} | ||
}], | ||
}) | ||
|
||
|
||
users.post("/users", async ({ set, body }) => { | ||
// Human user uses the User role | ||
const humanUserId = await createHumanUser(body); | ||
if (humanUserId) { | ||
const humanUserToken = await createToken(humanUserId); | ||
|
||
set.status = USER_CREATED_SUCCESSFULLY.code; | ||
return { | ||
...USER_CREATED_SUCCESSFULLY, | ||
token: humanUserToken, | ||
}; | ||
} | ||
|
||
set.status = UNSUCCESSFUL_USER_CREATION.code; | ||
return UNSUCCESSFUL_USER_CREATION; | ||
}, { | ||
body: t.Object({ | ||
email: t.String(), | ||
name: t.String(), | ||
}), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
export type User = { | ||
id: string; | ||
name: string; | ||
description: string; | ||
email: string; | ||
}; | ||
|
||
export type HumanUserBody = Omit<User, 'id'>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters