feat: added passwort change endpoint; finishing touches
This commit is contained in:
parent
6ed943ef20
commit
b72ecc55e9
38
README.md
38
README.md
@ -1,2 +1,36 @@
|
|||||||
Run tests:
|
# Simple Chat
|
||||||
`python -m unittest discover -s simple_chat_api/tests -p "*.py"`
|
|
||||||
|
This is a simple one-room chat application with user management, developed as a school project for study purposes.
|
||||||
|
|
||||||
|
**Not recommended for production use.**
|
||||||
|
|
||||||
|
A test protocol can be found here: [simple_chat_testprotokoll.pdf](simple_chat_testprotokoll.pdf)
|
||||||
|
|
||||||
|
## Running with Docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose -f docker/compose.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running in Development Mode
|
||||||
|
|
||||||
|
With Docker:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BUILD_TARGET=dev docker compose -f docker/compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, install dependencies for the frontend and start the development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
For the backend, install dependencies and run the server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r simple_chat_api/requirements.txt
|
||||||
|
python -m simple_chat_api
|
||||||
|
```
|
||||||
|
|||||||
@ -25,8 +25,6 @@ RUN rm /etc/nginx/nginx.conf
|
|||||||
RUN ln -s /simple-chat/docker/nginx.conf /etc/nginx/nginx.conf
|
RUN ln -s /simple-chat/docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
RUN mkdir -p /var/nginx
|
RUN mkdir -p /var/nginx
|
||||||
|
|
||||||
COPY docker/default.sqlite /simple-chat/data/data.sqlite
|
|
||||||
|
|
||||||
# Dev stage
|
# Dev stage
|
||||||
FROM base AS dev
|
FROM base AS dev
|
||||||
ARG ROOT_PASSWD
|
ARG ROOT_PASSWD
|
||||||
@ -53,6 +51,6 @@ RUN ssh-keygen -A && \
|
|||||||
CMD ["bash", "/entrypoint.sh"]
|
CMD ["bash", "/entrypoint.sh"]
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS prod
|
||||||
# Run entrypoint
|
# Run entrypoint
|
||||||
CMD ["bash", "/entrypoint.sh"]
|
CMD ["bash", "/entrypoint.sh"]
|
||||||
|
|||||||
18
docker/compose.yml
Normal file
18
docker/compose.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
simple_chat:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
target: ${BUILD_TARGET:-prod}
|
||||||
|
container_name: simple_chat
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- ROOT_PASSWD=adminpass
|
||||||
|
- JWT_SECRET=change_me
|
||||||
|
volumes:
|
||||||
|
- data:/simple-chat/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
Binary file not shown.
@ -17,20 +17,24 @@ p {
|
|||||||
|
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
@apply w-full p-2 border border-gray-500 bg-teal-200 rounded hover:bg-teal-300 focus:bg-teal-300 focus:outline-none focus:ring-2 focus:ring-teal-500;
|
@apply w-full p-2 border cursor-text border-gray-500 bg-teal-200 rounded hover:bg-teal-300 focus:bg-teal-300 focus:outline-none focus:ring-2 focus:ring-teal-500;
|
||||||
}
|
}
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
@apply text-gray-500 hover:text-gray-700;
|
@apply text-gray-500 hover:text-gray-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@apply w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600;
|
@apply w-full cursor-pointer bg-blue-500 text-white p-2 rounded hover:bg-blue-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@apply w-full h-screen flex items-center justify-center;
|
@apply w-full h-screen flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main > div {
|
||||||
|
@apply overflow-scroll max-h-full justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
.boxed {
|
.boxed {
|
||||||
@apply space-y-2 bg-blue-200 p-4 rounded-2xl shadow-lg;
|
@apply space-y-2 bg-blue-200 p-4 rounded-2xl shadow-lg;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,8 @@ import router from '@/router'
|
|||||||
>
|
>
|
||||||
<div class="subbox">
|
<div class="subbox">
|
||||||
<h3 @click="() => router.push('/')" class="hover:cursor-pointer">Simple Chat</h3>
|
<h3 @click="() => router.push('/')" class="hover:cursor-pointer">Simple Chat</h3>
|
||||||
<button
|
<button v-if="primaryUser.currentUser.value" @click="() => router.push('settings')">
|
||||||
v-if="primaryUser.currentUser.value && primaryUser.currentUser.value.role === 'admin'"
|
{{ primaryUser.currentUser.value.role === 'admin' ? 'Admin Panel' : 'Settings' }}
|
||||||
@click="() => router.push('admin')"
|
|
||||||
>
|
|
||||||
Admin Panel
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="subbox" v-if="primaryUser.currentUser.value">
|
<div class="subbox" v-if="primaryUser.currentUser.value">
|
||||||
|
|||||||
@ -21,6 +21,29 @@ export const deleteUser = async (user: User): Promise<void> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const changePassword = async (oldPassword: string, newPassword: string): Promise<string> => {
|
||||||
|
const response = await fetch(`${API_URL}/user/changePassword`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
return data.message
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => null)
|
||||||
|
throw new Error(
|
||||||
|
'Failed to change password. ' + (errorData && errorData.error ? errorData.error : ''),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const addUser = async (
|
export const addUser = async (
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
|
|||||||
@ -16,8 +16,8 @@ const router = createRouter({
|
|||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/settings',
|
||||||
name: 'admin',
|
name: 'settings',
|
||||||
component: () => import('../views/User.vue'),
|
component: () => import('../views/User.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { API_URL } from '@/main.ts'
|
import { API_URL } from '@/main.ts'
|
||||||
import { type User, primaryUser } from '@/composable/auth.ts'
|
import { type User, primaryUser } from '@/composable/auth.ts'
|
||||||
import { deleteUser, addUser } from '@/composable/settings'
|
import { deleteUser, addUser, changePassword } from '@/composable/settings'
|
||||||
|
|
||||||
import UserInfo from '@/components/UserInfo.vue'
|
import UserInfo from '@/components/UserInfo.vue'
|
||||||
|
|
||||||
@ -11,6 +11,9 @@ const new_user_passwd = ref('')
|
|||||||
const new_admin = ref(false)
|
const new_admin = ref(false)
|
||||||
const userCreationMsg = ref({ message: '', type: 'info' })
|
const userCreationMsg = ref({ message: '', type: 'info' })
|
||||||
const userDeletionMsg = ref({ message: '', type: 'info' })
|
const userDeletionMsg = ref({ message: '', type: 'info' })
|
||||||
|
const changePassMsg = ref({ message: '', type: 'info' })
|
||||||
|
|
||||||
|
const changePassField = ref({ old: '', new: '' })
|
||||||
|
|
||||||
const onNewUserCreation = async () => {
|
const onNewUserCreation = async () => {
|
||||||
addUser(new_user_name.value, new_user_passwd.value, new_admin.value)
|
addUser(new_user_name.value, new_user_passwd.value, new_admin.value)
|
||||||
@ -25,6 +28,19 @@ const onNewUserCreation = async () => {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onChangePassword = async () => {
|
||||||
|
changePassword(changePassField.value.old, changePassField.value.new)
|
||||||
|
.then((data: string) => {
|
||||||
|
changePassMsg.value = { message: data, type: 'success' }
|
||||||
|
changePassField.value.old = ''
|
||||||
|
changePassField.value.new = ''
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
changePassMsg.value = { message: `${error}`, type: 'error' }
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -35,6 +51,19 @@ const onNewUserCreation = async () => {
|
|||||||
<p><a class="font-bold">Name:</a> {{ primaryUser.currentUser.value.user }}</p>
|
<p><a class="font-bold">Name:</a> {{ primaryUser.currentUser.value.user }}</p>
|
||||||
<p><a class="font-bold">Role:</a> {{ primaryUser.currentUser.value.role }}</p>
|
<p><a class="font-bold">Role:</a> {{ primaryUser.currentUser.value.role }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="boxed w-80">
|
||||||
|
<h3>Change Own Password</h3>
|
||||||
|
<input v-model="changePassField.old" type="password" placeholder="Current Password" />
|
||||||
|
<input v-model="changePassField.new" type="password" placeholder="New Password" />
|
||||||
|
<button @click="() => onChangePassword()">Change Password</button>
|
||||||
|
<UserInfo :type="changePassMsg.type as any" v-if="changePassMsg.message">
|
||||||
|
<template #default>
|
||||||
|
<p>{{ changePassMsg.message }}</p>
|
||||||
|
</template>
|
||||||
|
</UserInfo>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template v-if="primaryUser.currentUser.value.role === 'admin'">
|
<template v-if="primaryUser.currentUser.value.role === 'admin'">
|
||||||
<div class="boxed">
|
<div class="boxed">
|
||||||
<h3>Users</h3>
|
<h3>Users</h3>
|
||||||
@ -88,9 +117,6 @@ const onNewUserCreation = async () => {
|
|||||||
</UserInfo>
|
</UserInfo>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
|
||||||
<p>You need Admin rights to see the rest...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,7 +2,8 @@ from passlib.context import CryptContext
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
JWT_SECRET = "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq"
|
JWT_SECRET = os.environ.get("JWT_SECRET", "F&M2eb%*T2dnhZqxw^ts6qotqF&M2eb%*T2dnhZqxw^ts6qotq")
|
||||||
|
debug = os.environ.get("DEV", "False").lower() in ("true", "1", "t")
|
||||||
hash_context = CryptContext(schemes=["bcrypt"])
|
hash_context = CryptContext(schemes=["bcrypt"])
|
||||||
db_url = os.environ.get("DATABASE_URL", "sqlite:///./data/data.sqlite")
|
db_url = os.environ.get("DATABASE_URL", "sqlite:///./data/data.sqlite")
|
||||||
use_optional_default_user = os.environ.get("CREATE_OPTIONAL_DEFAULT_USER", "False").lower() in ("true", "1", "t")
|
use_optional_default_user = os.environ.get("CREATE_OPTIONAL_DEFAULT_USER", "False").lower() in ("true", "1", "t")
|
||||||
|
|||||||
@ -71,6 +71,13 @@ class DbConnector:
|
|||||||
self.session.delete(user)
|
self.session.delete(user)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
def change_password(self, name: str, new_hash: str):
|
||||||
|
user = self.get_user(name)
|
||||||
|
if not user:
|
||||||
|
raise ValueError("User does not exist")
|
||||||
|
user.hash = new_hash
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
def add_msg_to_room(self, room: str, msg: str, user: str):
|
def add_msg_to_room(self, room: str, msg: str, user: str):
|
||||||
new_msg = Message(room=room, content=msg, user=user, timestamp=int(time.time()))
|
new_msg = Message(room=room, content=msg, user=user, timestamp=int(time.time()))
|
||||||
self.session.add(new_msg)
|
self.session.add(new_msg)
|
||||||
|
|||||||
@ -110,6 +110,22 @@ def add_user(user):
|
|||||||
response.status = 400
|
response.status = 400
|
||||||
return dumps({"error": str(e)})
|
return dumps({"error": str(e)})
|
||||||
|
|
||||||
|
@app.route("/changePassword", method=["POST"])
|
||||||
|
@user_guard()
|
||||||
|
def change_password(user):
|
||||||
|
data = read_keys_from_request(["new_password", "old_password"])
|
||||||
|
response.content_type = 'application/json'
|
||||||
|
try:
|
||||||
|
if not hash_context.verify(data["old_password"], user.hash):
|
||||||
|
response.status = 400
|
||||||
|
return dumps({"error": "Old password is incorrect"})
|
||||||
|
request.db_connector.change_password(user.name, hash_context.hash(data["new_password"]))
|
||||||
|
response.status = 200
|
||||||
|
return dumps({"message": "Password changed successfully"})
|
||||||
|
except ValueError as e:
|
||||||
|
response.status = 400
|
||||||
|
return dumps({"error": str(e)})
|
||||||
|
|
||||||
@app.route("/delete/<deletion_target>", method=["POST"])
|
@app.route("/delete/<deletion_target>", method=["POST"])
|
||||||
@admin_guard()
|
@admin_guard()
|
||||||
def delete_user(_, deletion_target: str):
|
def delete_user(_, deletion_target: str):
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from simple_chat_api.db_handler import DbConnector
|
|||||||
|
|
||||||
import simple_chat_api.endpoints.auth as auth
|
import simple_chat_api.endpoints.auth as auth
|
||||||
import simple_chat_api.endpoints.messages as messages
|
import simple_chat_api.endpoints.messages as messages
|
||||||
|
import simple_chat_api.config as config
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
# Needet because of: https://github.com/pyca/bcrypt/issues/684
|
# Needet because of: https://github.com/pyca/bcrypt/issues/684
|
||||||
@ -14,7 +15,7 @@ app = Bottle()
|
|||||||
|
|
||||||
|
|
||||||
def initialize_app():
|
def initialize_app():
|
||||||
db = DbConnector("sqlite:///./data/data.sqlite")
|
db = DbConnector(config.db_url)
|
||||||
|
|
||||||
@app.hook('before_request')
|
@app.hook('before_request')
|
||||||
def attach_resources():
|
def attach_resources():
|
||||||
@ -31,4 +32,4 @@ def main():
|
|||||||
|
|
||||||
root_app = Bottle()
|
root_app = Bottle()
|
||||||
root_app.mount('/api', app)
|
root_app.mount('/api', app)
|
||||||
root_app.run(host='localhost', port=7000, debug=True)
|
root_app.run(host='localhost', port=7000, debug=config.debug)
|
||||||
@ -5,9 +5,17 @@ import time
|
|||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import requests
|
import requests
|
||||||
|
import unittest
|
||||||
|
|
||||||
server_process = None
|
server_process = None
|
||||||
API_URL = "http://localhost:7000/api"
|
API_URL = "http://localhost:7000/api"
|
||||||
|
DB_PATH = "./data/TEST.sqlite"
|
||||||
|
|
||||||
|
users = {
|
||||||
|
"admin": "admin",
|
||||||
|
"max": "12345"
|
||||||
|
}
|
||||||
|
|
||||||
def setUpModule():
|
def setUpModule():
|
||||||
"""Start the API server in a separate process before running tests"""
|
"""Start the API server in a separate process before running tests"""
|
||||||
def run_server():
|
def run_server():
|
||||||
@ -15,7 +23,7 @@ def setUpModule():
|
|||||||
# Needet to get python3 dir
|
# Needet to get python3 dir
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env.update({
|
env.update({
|
||||||
"DATABASE_URL": "sqlite:///./data/TEST.sqlite",
|
"DATABASE_URL": f"sqlite:///{DB_PATH}",
|
||||||
"CREATE_OPTIONAL_DEFAULT_USER": "true"
|
"CREATE_OPTIONAL_DEFAULT_USER": "true"
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -40,9 +48,20 @@ def tearDownModule():
|
|||||||
if server_process:
|
if server_process:
|
||||||
server_process.send_signal(signal.SIGTERM)
|
server_process.send_signal(signal.SIGTERM)
|
||||||
server_process.wait()
|
server_process.wait()
|
||||||
|
os.remove(DB_PATH)
|
||||||
|
|
||||||
|
def build_session(user: str, password: str) -> requests.Session:
|
||||||
|
session = requests.Session()
|
||||||
|
response = session.post(f"{API_URL}/user/token", json={
|
||||||
|
"user": user,
|
||||||
|
"password": password
|
||||||
|
})
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise ValueError(f"Failed to get token for user {user}; {response.text}")
|
||||||
|
return [session, response.json()]
|
||||||
|
|
||||||
|
|
||||||
class TestServer(unittest.TestCase):
|
class TestServer(unittest.TestCase):
|
||||||
|
|
||||||
def test_online(self):
|
def test_online(self):
|
||||||
"""Test if the server is running"""
|
"""Test if the server is running"""
|
||||||
response = requests.get(API_URL)
|
response = requests.get(API_URL)
|
||||||
@ -50,25 +69,19 @@ class TestServer(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestAuthEndpoints(unittest.TestCase):
|
class TestAuthEndpoints(unittest.TestCase):
|
||||||
|
userSessions = {}
|
||||||
def __init__(self, methodName = "runTest"):
|
def __init__(self, methodName = "runTest"):
|
||||||
super().__init__(methodName)
|
super().__init__(methodName)
|
||||||
self.users = {
|
|
||||||
"admin": "admin",
|
|
||||||
"max": "12345"
|
|
||||||
}
|
|
||||||
self.userSessions = {}
|
|
||||||
|
|
||||||
def test_get_token(self):
|
def test_1_get_token(self):
|
||||||
"""Test the /token endpoint"""
|
"""Test the /token endpoint"""
|
||||||
for user,password in self.users.items():
|
for user,password in users.items():
|
||||||
with self.subTest(user=user):
|
with self.subTest(user=user):
|
||||||
self.userSessions[user] = requests.Session()
|
try:
|
||||||
response = self.userSessions[user].post(f"{API_URL}/user/token", json={
|
session, data = build_session(user, password)
|
||||||
"user": user,
|
self.userSessions[user] = session
|
||||||
"password": password
|
except ValueError as e:
|
||||||
})
|
self.fail(str(e))
|
||||||
self.assertEqual(response.status_code, 200, f"Failed to get token for user {user}; {response.text}")
|
|
||||||
data = response.json()
|
|
||||||
excepted = {
|
excepted = {
|
||||||
"sub": {
|
"sub": {
|
||||||
"user": user,
|
"user": user,
|
||||||
@ -76,3 +89,191 @@ class TestAuthEndpoints(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
self.assertEqual(data["sub"], excepted["sub"], f"Token content mismatch for user {user}")
|
self.assertEqual(data["sub"], excepted["sub"], f"Token content mismatch for user {user}")
|
||||||
|
|
||||||
|
def test_2_get_user(self):
|
||||||
|
if self.userSessions == {}:
|
||||||
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
for user,session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
response = session.get(f"{API_URL}/user")
|
||||||
|
self.assertEqual(response.status_code, 200, f"Failed to get user info for {user}; {response.text}")
|
||||||
|
data = response.json()
|
||||||
|
excepted = {
|
||||||
|
"name": user,
|
||||||
|
}
|
||||||
|
self.assertEqual(data, excepted, f"User info mismatch for {user}")
|
||||||
|
|
||||||
|
def test_3_get_all(self):
|
||||||
|
if self.userSessions == {}:
|
||||||
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
|
for user,session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
response = session.get(f"{API_URL}/user/getAll")
|
||||||
|
if user == "admin":
|
||||||
|
self.assertEqual(response.status_code, 200, f"Failed to get all users for admin; {response.text}")
|
||||||
|
data = response.json()
|
||||||
|
excepted = [
|
||||||
|
{"name": "admin", "role": "admin"},
|
||||||
|
{"name": "max", "role": "user"}
|
||||||
|
]
|
||||||
|
self.assertTrue(all(item in data for item in excepted), f"User list mismatch for admin")
|
||||||
|
else:
|
||||||
|
self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not access /getAll; {response.text}")
|
||||||
|
|
||||||
|
def test_4_create_user(self):
|
||||||
|
if self.userSessions == {}:
|
||||||
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
|
new_users = [{
|
||||||
|
"new_user": "newuser",
|
||||||
|
"new_password": "newpass",
|
||||||
|
"new_admin": False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"new_user": "admin2",
|
||||||
|
"new_password": "adminpass",
|
||||||
|
"new_admin": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"new_user": "invalid/user",
|
||||||
|
"new_password": "pass",
|
||||||
|
"new_admin": False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for user,session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
if user == "admin":
|
||||||
|
for new_user in new_users:
|
||||||
|
if "invalid" in new_user["new_user"]:
|
||||||
|
response = session.post(f"{API_URL}/user/add", json=new_user)
|
||||||
|
self.assertEqual(response.status_code, 400, f"Creating user with invalid name should fail; {response.text}")
|
||||||
|
continue
|
||||||
|
response = session.post(f"{API_URL}/user/add", json=new_user)
|
||||||
|
self.assertEqual(response.status_code, 200, f"Admin failed to create new user; {response.text}")
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("created successfully", data["message"], f"Created user info mismatch")
|
||||||
|
|
||||||
|
response = session.post(f"{API_URL}/user/add", json=new_user)
|
||||||
|
self.assertEqual(response.status_code, 400, f"Creating duplicate user should fail; {response.text}")
|
||||||
|
else:
|
||||||
|
self.assertEqual(response.status_code, 400, f"Non-admin user {user} should not create users; {response.text}")
|
||||||
|
|
||||||
|
def test_5_delete_user(self):
|
||||||
|
if self.userSessions == {}:
|
||||||
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
|
for user,session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
if user == "admin":
|
||||||
|
response = session.post(f"{API_URL}/user/delete/kim")
|
||||||
|
self.assertEqual(response.status_code, 200, f"Admin failed to delete user kim; {response.text}")
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("deleted successfully", data["message"], f"Deleted user info mismatch")
|
||||||
|
|
||||||
|
response = session.post(f"{API_URL}/user/delete/nonexistent")
|
||||||
|
self.assertEqual(response.status_code, 400, f"Deleting non-existent user should fail; {response.text}")
|
||||||
|
else:
|
||||||
|
response = session.post(f"{API_URL}/user/delete/max")
|
||||||
|
self.assertEqual(response.status_code, 401, f"Non-admin user {user} should not delete users; {response.text}")
|
||||||
|
|
||||||
|
def test_6_change_password(self):
|
||||||
|
if self.userSessions == {}:
|
||||||
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
|
for user,session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
pyload = {
|
||||||
|
"new_password": "newadminpass"
|
||||||
|
}
|
||||||
|
response = session.post(f"{API_URL}/user/changePassword/", json=pyload)
|
||||||
|
self.assertEqual(response.status_code, 200, f"Admin failed to change password; {response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMessage(unittest.TestCase):
|
||||||
|
last_msg = None
|
||||||
|
userSessions = {}
|
||||||
|
messages = [
|
||||||
|
{"content": "Admin here, managing things."},
|
||||||
|
{"content": "Hello, this is max!"},
|
||||||
|
]
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
for user, password in users.items():
|
||||||
|
cls.userSessions[user], _ = build_session(user, password)
|
||||||
|
|
||||||
|
def test_1_post_message(self):
|
||||||
|
if not self.userSessions:
|
||||||
|
self.skipTest("No user sessions available. Run test_get_token first.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for i, (user, session) in enumerate(self.userSessions.items()):
|
||||||
|
with self.subTest(user=user):
|
||||||
|
response = session.post(f"{API_URL}/messages/general", json=self.messages[i])
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
200,
|
||||||
|
f"User {user} failed to post message; {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
clear_json = response.json()
|
||||||
|
|
||||||
|
msg_id, data = list(clear_json.items())[-1]
|
||||||
|
|
||||||
|
TestMessage.last_msg = {
|
||||||
|
"message_id": msg_id,
|
||||||
|
"timestamp": data["timestamp"],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
self.messages[i]["content"],
|
||||||
|
data["content"],
|
||||||
|
f"Posted message response missing content for user {user}"
|
||||||
|
)
|
||||||
|
time.sleep(1) # Ensure different timestamps
|
||||||
|
|
||||||
|
def test_2_get_messages_since(self):
|
||||||
|
if not self.userSessions or self.last_msg is None:
|
||||||
|
self.skipTest("No user sessions available or no messages posted. Run previous tests first.")
|
||||||
|
|
||||||
|
for user, session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
response = session.get(f"{API_URL}/messages/general?since={self.last_msg['timestamp']}")
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
200,
|
||||||
|
f"User {user} failed to get messages since timestamp; {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
first_key = list(response.json().keys())[0]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
first_key,
|
||||||
|
self.last_msg["message_id"], # match full message dict
|
||||||
|
f"User {user} did not receive the expected message"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_3_get_all_messages(self):
|
||||||
|
if not self.userSessions or self.last_msg is None:
|
||||||
|
self.skipTest("No user sessions available or no messages posted. Run previous tests first.")
|
||||||
|
|
||||||
|
for user, session in self.userSessions.items():
|
||||||
|
with self.subTest(user=user):
|
||||||
|
response = session.get(f"{API_URL}/messages/general")
|
||||||
|
self.assertEqual(
|
||||||
|
response.status_code,
|
||||||
|
200,
|
||||||
|
f"User {user} failed to get all messages; {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
messages_data = response.json()
|
||||||
|
message_contents = [msg["content"] for _, msg in messages_data.items()]
|
||||||
|
|
||||||
|
expected_contents = [msg["content"] for msg in self.messages]
|
||||||
|
for content in expected_contents:
|
||||||
|
self.assertIn(content, message_contents, f"Message with content '{content}' not found")
|
||||||
|
|
||||||
|
self.assertEqual(len(messages_data), len(self.messages), "Number of messages doesn't match expected")
|
||||||
|
|
||||||
BIN
simple_chat_testprotokoll.pdf
Normal file
BIN
simple_chat_testprotokoll.pdf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user