Initial commit: nimforum with BaraDB backend
CI / test_stable_and_devel (stable) (push) Has been cancelled
CI / test_stable_and_devel (stable, devel) (push) Has been cancelled
CI / test_stable_and_devel (stable, stable) (push) Has been cancelled

- Replaced SQLite with BaraDB via TCP wire protocol
- Added baradb_client.nim and baradb_sqlite.nim adapter
- Renamed SQL keywords: status->usrStatus, key->sessionKey, like->postLike
- Added NOW() support for datetime defaults
- Switched Jester to asynchttpserver (-d:useStdLib) to avoid thread/GC issues
- Frontend built and favicon added
This commit is contained in:
2026-05-15 17:24:24 +03:00
commit 570d53f391
80 changed files with 29916 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
import unittest, common
import webdriver
import karaxutils
proc selectCategory(session: Session, name: string) =
with session:
click "#category-selection .dropdown-toggle"
click "#category-selection ." & name
proc createCategory(session: Session, baseUrl, name, color, description: string) =
with session:
navigate baseUrl
click "#categories-btn"
ensureExists "#add-category"
click "#add-category .plus-btn"
clear "#add-category input[name='name']"
clear "#add-category input[name='description']"
sendKeys "#add-category input[name='name']", name
setColor "#add-category input[name='color']", color
sendKeys "#add-category input[name='description']", description
click "#add-category #add-category-btn"
checkText "#category-" & name.slug(), name
proc categoriesUserTests(session: Session, baseUrl: string) =
let
title = "Category Test"
content = "Choosing category test"
suite "user tests":
with session:
navigate baseUrl
login "user", "user"
setup:
with session:
navigate baseUrl
test "no category add available":
with session:
click "#new-thread-btn"
checkIsNone "#add-category"
test "no category add available category page":
with session:
click "#categories-btn"
checkIsNone "#add-category"
test "can create category thread":
with session:
click "#new-thread-btn"
sendKeys "#thread-title", title
selectCategory "fun"
sendKeys "#reply-textarea", content
click "#create-thread-btn"
checkText "#thread-title .category", "Fun"
navigate baseUrl
ensureExists title, LinkTextSelector
test "can create category thread and change category":
with session:
let newTitle = title & " Selection"
click "#new-thread-btn"
sendKeys "#thread-title", newTitle
selectCategory "fun"
sendKeys "#reply-textarea", content
click "#create-thread-btn"
checkText "#thread-title .category", "Fun"
selectCategory "announcements"
checkText "#thread-title .category", "Announcements"
# Make sure there is no error
checkIsNone "#thread-title .text-error"
navigate baseUrl
ensureExists newTitle, LinkTextSelector
test "can navigate to categories page":
with session:
click "#categories-btn"
ensureExists "#categories-list"
test "can view post under category":
with session:
# create a few threads
click "#new-thread-btn"
sendKeys "#thread-title", "Post 1"
selectCategory "fun"
sendKeys "#reply-textarea", "Post 1"
click "#create-thread-btn"
navigate baseUrl
click "#new-thread-btn"
sendKeys "#thread-title", "Post 2"
selectCategory "announcements"
sendKeys "#reply-textarea", "Post 2"
click "#create-thread-btn"
navigate baseUrl
click "#new-thread-btn"
sendKeys "#thread-title", "Post 3"
selectCategory "unsorted"
sendKeys "#reply-textarea", "Post 3"
click "#create-thread-btn"
navigate baseUrl
click "#categories-btn"
ensureExists "#categories-list"
click "#category-unsorted"
checkText "#threads-list .thread-title a", "Post 3"
for element in session.waitForElements("#threads-list .category-name"):
# Have to user "innerText" because elements are hidden on this page
assert element.getProperty("innerText") == "Unsorted"
selectCategory "announcements"
checkText "#threads-list .thread-title a", "Post 2"
for element in session.waitForElements("#threads-list .category-name"):
assert element.getProperty("innerText") == "Announcements"
selectCategory "fun"
checkText "#threads-list .thread-title a", "Post 1"
for element in session.waitForElements("#threads-list .category-name"):
assert element.getProperty("innerText") == "Fun"
session.logout()
proc categoriesAdminTests(session: Session, baseUrl: string) =
suite "admin tests":
with session:
navigate baseUrl
login "admin", "admin"
test "can create category via dropdown":
let
name = "Category Test"
color = "#720904"
description = "This is a description"
with session:
click "#new-thread-btn"
ensureExists "#add-category"
click "#add-category .plus-btn"
clear "#add-category input[name='name']"
clear "#add-category input[name='description']"
sendKeys "#add-category input[name='name']", name
setColor "#add-category input[name='color']", color
sendKeys "#add-category input[name='description']", description
click "#add-category #add-category-btn"
checkText "#category-selection .selected-category", name
test "can create category on category page":
let
name = "Category Test Page"
color = "#70B4D4"
description = "This is a description on category page"
with session:
createCategory baseUrl, name, color, description
test "category adding disabled on admin logout":
with session:
navigate(baseUrl & "c/0")
ensureExists "#add-category"
logout()
checkIsNone "#add-category"
navigate baseUrl
login "admin", "admin"
session.logout()
proc test*(session: Session, baseUrl: string) =
session.navigate(baseUrl)
categoriesUserTests(session, baseUrl)
categoriesAdminTests(session, baseUrl)
session.navigate(baseUrl)
+189
View File
@@ -0,0 +1,189 @@
import os, options, unittest, strutils
import webdriver
import macros
const actionDelayMs {.intdefine.} = 0
## Inserts a delay in milliseconds between automated actions. Useful for debugging tests
macro with*(obj: typed, code: untyped): untyped =
## Execute a set of statements with an object
expectKind code, nnkStmtList
template checkCompiles(res, default) =
when compiles(res):
res
else:
default
result = code.copy
# Simply inject obj into call
for i in 0 ..< result.len:
if result[i].kind in {nnkCommand, nnkCall}:
result[i].insert(1, obj)
result = getAst(checkCompiles(result, code))
proc elementIsSome(element: Option[Element]): bool =
return element.isSome
proc elementIsNone(element: Option[Element]): bool =
return element.isNone
proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50,
waitCondition: proc(element: Option[Element]): bool = elementIsSome): Option[Element]
proc click*(session: Session, element: string, strategy=CssSelector) =
let el = session.waitForElement(element, strategy)
el.get().click()
proc sendKeys*(session: Session, element, keys: string) =
let el = session.waitForElement(element)
el.get().sendKeys(keys)
proc clear*(session: Session, element: string) =
let el = session.waitForElement(element)
el.get().clear()
proc sendKeys*(session: Session, element: string, keys: varargs[Key]) =
let el = session.waitForElement(element)
# focus
el.get().click()
for key in keys:
session.press(key)
proc ensureExists*(session: Session, element: string, strategy=CssSelector) =
discard session.waitForElement(element, strategy)
template check*(session: Session, element: string, function: untyped) =
let el = session.waitForElement(element)
check function(el)
template check*(session: Session, element: string,
strategy: LocationStrategy, function: untyped) =
let el = session.waitForElement(element, strategy)
check function(el)
proc setColor*(session: Session, element, color: string, strategy=CssSelector) =
let el = session.waitForElement(element, strategy)
discard session.execute("arguments[0].setAttribute('value', '" & color & "')", el.get())
proc checkIsNone*(session: Session, element: string, strategy=CssSelector) =
discard session.waitForElement(element, strategy, waitCondition=elementIsNone)
template checkText*(session: Session, element, expectedValue: string) =
let el = session.waitForElement(element)
check el.get().getText() == expectedValue
proc waitForElement*(
session: Session, selector: string, strategy=CssSelector,
timeout=20000, pollTime=50,
waitCondition: proc(element: Option[Element]): bool = elementIsSome
): Option[Element] =
var waitTime = 0
when actionDelayMs > 0:
sleep(actionDelayMs)
while true:
try:
let loading = session.findElement(selector, strategy)
if waitCondition(loading):
return loading
finally:
discard
sleep(pollTime)
waitTime += pollTime
if waitTime > timeout:
doAssert false, "Wait for load time exceeded"
proc waitForElements*(
session: Session, selector: string, strategy=CssSelector,
timeout=20000, pollTime=50
): seq[Element] =
var waitTime = 0
when actionDelayMs > 0:
sleep(actionDelayMs)
while true:
let loading = session.findElements(selector, strategy)
if loading.len > 0:
return loading
sleep(pollTime)
waitTime += pollTime
if waitTime > timeout:
doAssert false, "Wait for load time exceeded"
proc setUserRank*(session: Session, baseUrl, user, rank: string) =
with session:
navigate(baseUrl & "profile/" & user)
click "#settings-tab"
click "#rank-field"
click("#rank-field option#rank-" & rank.toLowerAscii)
click "#save-btn"
proc logout*(session: Session) =
with session:
click "#profile-btn"
click "#profile-btn #logout-btn"
# Verify we have logged out by looking for the log in button.
ensureExists "#login-btn"
proc login*(session: Session, user, password: string) =
with session:
click "#login-btn"
clear "#login-form input[name='username']"
clear "#login-form input[name='password']"
sendKeys "#login-form input[name='username']", user
sendKeys "#login-form input[name='password']", password
sendKeys "#login-form input[name='password']", Key.Enter
# Verify that the user menu has been initialised properly.
click "#profile-btn"
checkText "#profile-btn #profile-name", user
click "#profile-btn"
proc register*(session: Session, user, password: string, verify = true) =
with session:
click "#signup-btn"
clear "#signup-form input[name='email']"
clear "#signup-form input[name='username']"
clear "#signup-form input[name='password']"
sendKeys "#signup-form input[name='email']", user & "@" & user & ".com"
sendKeys "#signup-form input[name='username']", user
sendKeys "#signup-form input[name='password']", password
click "#signup-modal .create-account-btn"
if verify:
with session:
# Verify that the user menu has been initialised properly.
click "#profile-btn"
checkText "#profile-btn #profile-name", user
# close menu
click "#profile-btn"
proc createThread*(session: Session, title, content: string) =
with session:
click "#new-thread-btn"
sendKeys "#thread-title", title
sendKeys "#reply-textarea", content
click "#create-thread-btn"
checkText "#thread-title .title-text", title
checkText ".original-post div.post-content", content
+36
View File
@@ -0,0 +1,36 @@
import unittest, common
import webdriver
proc test*(session: Session, baseUrl: string) =
session.navigate(baseUrl)
test "can see banned posts":
with session:
register("issue181", "issue181")
logout()
# Change rank to `user` so they can post.
login("admin", "admin")
setUserRank(baseUrl, "issue181", "user")
logout()
login("issue181", "issue181")
navigate(baseUrl)
const title = "Testing issue 181."
createThread(title, "Test for issue #181")
logout()
login("admin", "admin")
# Ban our user.
setUserRank(baseUrl, "issue181", "banned")
# Make sure the banned user's thread is still visible.
navigate(baseUrl)
ensureExists("tr.banned")
checkText("tr.banned .thread-title > a", title)
logout()
checkText("tr.banned .thread-title > a", title)
+43
View File
@@ -0,0 +1,43 @@
import unittest, common
import webdriver
proc test*(session: Session, baseUrl: string) =
session.navigate(baseUrl)
# Sanity checks
test "shows sign up":
session.checkText("#signup-btn", "Sign up")
test "shows log in":
session.checkText("#login-btn", "Log in")
test "is empty":
session.checkIsNone("tr > td.thread-title")
# Logging in
test "can login/logout":
with session:
login("admin", "admin")
# Check whether we can log out.
logout()
# Verify we have logged out by looking for the log in button.
ensureExists "#login-btn"
test "can register":
with session:
register("test", "test")
logout()
test "can't register same username with different case":
with session:
register "test1", "test1", verify = false
logout()
navigate baseUrl
register "TEst1", "test1", verify = false
ensureExists "#signup-form .has-error"
navigate baseUrl
+267
View File
@@ -0,0 +1,267 @@
import unittest, common
import webdriver
let
userTitleStr = "This is a user thread!"
userContentStr = "A user has filled this out"
adminTitleStr = "This is a thread title!"
adminContentStr = "This is content"
proc banUser(session: Session, baseUrl: string) =
with session:
login "admin", "admin"
setUserRank baseUrl, "user", "banned"
logout()
proc unBanUser(session: Session, baseUrl: string) =
with session:
login "admin", "admin"
setUserRank baseUrl, "user", "user"
logout()
proc userTests(session: Session, baseUrl: string) =
suite "user thread tests":
session.login("user", "user")
setup:
session.navigate(baseUrl)
test "can create thread":
with session:
click "#new-thread-btn"
sendKeys "#thread-title", userTitleStr
sendKeys "#reply-textarea", userContentStr
click "#create-thread-btn"
checkText "#thread-title .title-text", userTitleStr
checkText ".original-post div.post-content", userContentStr
test "can delete thread":
with session:
# create thread to be deleted
click "#new-thread-btn"
sendKeys "#thread-title", "To be deleted"
sendKeys "#reply-textarea", "This thread is to be deleted"
click "#create-thread-btn"
click ".post-buttons .delete-button"
# click delete confirmation
click "#delete-modal .delete-btn"
# Make sure the forum post is gone
checkIsNone "To be deleted", LinkTextSelector
test "cannot (un)pin thread":
with session:
navigate(baseUrl)
click "#new-thread-btn"
sendKeys "#thread-title", "Unpinnable"
sendKeys "#reply-textarea", "Cannot (un)pin as an user"
click "#create-thread-btn"
checkIsNone "#pin-btn"
test "cannot lock threads":
with session:
navigate(baseUrl)
click "#new-thread-btn"
sendKeys "#thread-title", "Locking"
sendkeys "#reply-textarea", "Cannot lock as an user"
click "#create-thread-btn"
checkIsNone "#lock-btn"
session.logout()
proc anonymousTests(session: Session, baseUrl: string) =
suite "anonymous user tests":
with session:
navigate baseUrl
test "can view banned thread":
with session:
ensureExists userTitleStr, LinkTextSelector
with session:
navigate baseUrl
proc bannedTests(session: Session, baseUrl: string) =
suite "banned user thread tests":
with session:
navigate baseUrl
login "banned", "banned"
test "can't start thread":
with session:
click "#new-thread-btn"
sendKeys "#thread-title", "test"
sendKeys "#reply-textarea", "test"
click "#create-thread-btn"
ensureExists "#new-thread p.text-error"
session.logout()
proc adminTests(session: Session, baseUrl: string) =
suite "admin thread tests":
session.login("admin", "admin")
setup:
session.navigate(baseUrl)
test "can view banned thread":
with session:
ensureExists userTitleStr, LinkTextSelector
test "can create thread":
with session:
click "#new-thread-btn"
sendKeys "#thread-title", adminTitleStr
sendKeys "#reply-textarea", adminContentStr
click "#create-thread-btn"
checkText "#thread-title .title-text", adminTitleStr
checkText ".original-post div.post-content", adminContentStr
test "try create duplicate thread":
with session:
click "#new-thread-btn"
ensureExists "#new-thread"
sendKeys "#thread-title", adminTitleStr
sendKeys "#reply-textarea", adminContentStr
click "#create-thread-btn"
ensureExists "#new-thread p.text-error"
test "can edit post":
let modificationText = " and I edited it!"
with session:
click adminTitleStr, LinkTextSelector
click ".post-buttons .edit-button"
sendKeys ".original-post #reply-textarea", modificationText
click ".edit-buttons .save-button"
checkText ".original-post div.post-content", adminContentStr & modificationText
test "can like thread":
# Try to like the user thread above
with session:
click userTitleStr, LinkTextSelector
click ".post-buttons .like-button"
checkText ".post-buttons .like-button .like-count", "1"
test "can delete thread":
with session:
click adminTitleStr, LinkTextSelector
click ".post-buttons .delete-button"
# click delete confirmation
click "#delete-modal .delete-btn"
# Make sure the forum post is gone
checkIsNone adminTitleStr, LinkTextSelector
test "can pin a thread":
with session:
click "#new-thread-btn"
sendKeys "#thread-title", "Pinned post"
sendKeys "#reply-textarea", "A pinned post"
click "#create-thread-btn"
navigate(baseUrl)
click "#new-thread-btn"
sendKeys "#thread-title", "Normal post"
sendKeys "#reply-textarea", "A normal post"
click "#create-thread-btn"
navigate(baseUrl)
click "Pinned post", LinkTextSelector
click "#pin-btn"
checkText "#pin-btn", "Unpin Thread"
navigate(baseUrl)
# Make sure pin exists
ensureExists "#threads-list .thread-1 .thread-title i"
checkText "#threads-list .thread-1 .thread-title a", "Pinned post"
checkText "#threads-list .thread-2 .thread-title a", "Normal post"
test "can unpin a thread":
with session:
click "Pinned post", LinkTextSelector
click "#pin-btn"
checkText "#pin-btn", "Pin Thread"
navigate(baseUrl)
checkIsNone "#threads-list .thread-2 .thread-title i"
checkText "#threads-list .thread-1 .thread-title a", "Normal post"
checkText "#threads-list .thread-2 .thread-title a", "Pinned post"
test "can lock a thread":
with session:
click "Locking", LinkTextSelector
click "#lock-btn"
ensureExists "#thread-title i.fas.fa-lock.fa-xs"
test "locked thread appears on frontpage":
with session:
click "#new-thread-btn"
sendKeys "#thread-title", "A new locked thread"
sendKeys "#reply-textarea", "This thread should appear locked on the frontpage"
click "#create-thread-btn"
click "#lock-btn"
navigate(baseUrl)
ensureExists "#threads-list .thread-1 .thread-title i.fas.fa-lock.fa-xs"
test "can unlock a thread":
with session:
click "Locking", LinkTextSelector
click "#lock-btn"
checkIsNone "#thread-title i.fas.fa-lock.fa-xs"
session.logout()
proc test*(session: Session, baseUrl: string) =
session.navigate(baseUrl)
userTests(session, baseUrl)
banUser(session, baseUrl)
bannedTests(session, baseUrl)
anonymousTests(session, baseUrl)
adminTests(session, baseUrl)
unBanUser(session, baseUrl)
session.navigate(baseUrl)