424 lines
9.1 KiB
Go
424 lines
9.1 KiB
Go
package store
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/urit/urit/internal/models"
|
|
)
|
|
|
|
// testStore creates a temporary SQLite store for testing.
|
|
// The database is removed after the test completes.
|
|
func testStore(t *testing.T) *SQLiteStore {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
|
|
s, err := OpenSQLite(path)
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { s.Close() })
|
|
|
|
return s
|
|
}
|
|
|
|
// --- User tests ---
|
|
|
|
func TestUserCRUD(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
// Create
|
|
user := &models.User{
|
|
Name: "TestUser",
|
|
PasswordHash: "$2a$10$fakehash",
|
|
Comments: "Test account",
|
|
Active: true,
|
|
SecStatus: 2,
|
|
SecBoard: 2,
|
|
SecLibrary: 2,
|
|
SecBulletin: 2,
|
|
TimeLimit: 3600,
|
|
LastOn: timePtr(time.Now()),
|
|
}
|
|
|
|
if err := s.CreateUser(user); err != nil {
|
|
t.Fatalf("CreateUser: %v", err)
|
|
}
|
|
if user.ID == 0 {
|
|
t.Fatal("CreateUser did not set ID")
|
|
}
|
|
|
|
// Read by ID
|
|
got, err := s.GetUser(user.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetUser: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("GetUser returned nil")
|
|
}
|
|
if got.Name != "TestUser" {
|
|
t.Errorf("Name = %q, want %q", got.Name, "TestUser")
|
|
}
|
|
if got.SecStatus != 2 {
|
|
t.Errorf("SecStatus = %d, want 2", got.SecStatus)
|
|
}
|
|
|
|
// Read by name (case-insensitive)
|
|
got, err = s.GetUserByName("testuser")
|
|
if err != nil {
|
|
t.Fatalf("GetUserByName: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("GetUserByName returned nil for case-insensitive match")
|
|
}
|
|
if got.ID != user.ID {
|
|
t.Errorf("ID = %d, want %d", got.ID, user.ID)
|
|
}
|
|
|
|
// Read nonexistent
|
|
got, err = s.GetUser(9999)
|
|
if err != nil {
|
|
t.Fatalf("GetUser(9999): %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Error("GetUser(9999) should return nil")
|
|
}
|
|
|
|
// Update
|
|
user.SecStatus = 255
|
|
user.MessagesPosted = 42
|
|
if err := s.UpdateUser(user); err != nil {
|
|
t.Fatalf("UpdateUser: %v", err)
|
|
}
|
|
got, _ = s.GetUser(user.ID)
|
|
if got.SecStatus != 255 {
|
|
t.Errorf("SecStatus after update = %d, want 255", got.SecStatus)
|
|
}
|
|
if got.MessagesPosted != 42 {
|
|
t.Errorf("MessagesPosted = %d, want 42", got.MessagesPosted)
|
|
}
|
|
|
|
// List
|
|
users, err := s.ListUsers(0, 100)
|
|
if err != nil {
|
|
t.Fatalf("ListUsers: %v", err)
|
|
}
|
|
if len(users) != 1 {
|
|
t.Errorf("ListUsers returned %d users, want 1", len(users))
|
|
}
|
|
|
|
// Count
|
|
count, err := s.CountUsers()
|
|
if err != nil {
|
|
t.Fatalf("CountUsers: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("CountUsers = %d, want 1", count)
|
|
}
|
|
|
|
// Delete (soft)
|
|
if err := s.DeleteUser(user.ID); err != nil {
|
|
t.Fatalf("DeleteUser: %v", err)
|
|
}
|
|
count, _ = s.CountUsers()
|
|
if count != 0 {
|
|
t.Errorf("CountUsers after delete = %d, want 0", count)
|
|
}
|
|
// Verify the record still exists but is inactive
|
|
got, _ = s.GetUser(user.ID)
|
|
if got == nil {
|
|
t.Fatal("Soft-deleted user should still be readable by ID")
|
|
}
|
|
if got.Active {
|
|
t.Error("Soft-deleted user should have Active=false")
|
|
}
|
|
}
|
|
|
|
func TestUserDuplicateName(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
user1 := &models.User{Name: "Sysop", Active: true}
|
|
if err := s.CreateUser(user1); err != nil {
|
|
t.Fatalf("CreateUser 1: %v", err)
|
|
}
|
|
|
|
user2 := &models.User{Name: "Sysop", Active: true}
|
|
err := s.CreateUser(user2)
|
|
if err == nil {
|
|
t.Fatal("Expected error for duplicate name, got nil")
|
|
}
|
|
}
|
|
|
|
// --- Board and Message tests ---
|
|
|
|
func TestBoardAndMessages(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
// Create board
|
|
board := &models.Board{
|
|
Name: "General",
|
|
ReadLow: 0,
|
|
ReadHigh: 255,
|
|
WriteLow: 1,
|
|
WriteHigh: 255,
|
|
MaxPosts: 100,
|
|
}
|
|
if err := s.CreateBoard(board); err != nil {
|
|
t.Fatalf("CreateBoard: %v", err)
|
|
}
|
|
if board.ID == 0 {
|
|
t.Fatal("CreateBoard did not set ID")
|
|
}
|
|
|
|
// List boards
|
|
boards, err := s.ListBoards()
|
|
if err != nil {
|
|
t.Fatalf("ListBoards: %v", err)
|
|
}
|
|
if len(boards) != 1 {
|
|
t.Errorf("ListBoards returned %d, want 1", len(boards))
|
|
}
|
|
|
|
// Create messages
|
|
msg1 := &models.Message{
|
|
BoardID: board.ID,
|
|
Title: "First Post",
|
|
Author: "Sysop",
|
|
AuthorID: 1,
|
|
Body: "Welcome to the BBS!",
|
|
}
|
|
if err := s.CreateMessage(msg1); err != nil {
|
|
t.Fatalf("CreateMessage 1: %v", err)
|
|
}
|
|
if msg1.Number != 1 {
|
|
t.Errorf("First message number = %d, want 1", msg1.Number)
|
|
}
|
|
|
|
msg2 := &models.Message{
|
|
BoardID: board.ID,
|
|
Title: "Reply",
|
|
Author: "User",
|
|
AuthorID: 2,
|
|
Body: "Thanks!",
|
|
ReplyTo: msg1.ID,
|
|
}
|
|
if err := s.CreateMessage(msg2); err != nil {
|
|
t.Fatalf("CreateMessage 2: %v", err)
|
|
}
|
|
if msg2.Number != 2 {
|
|
t.Errorf("Second message number = %d, want 2", msg2.Number)
|
|
}
|
|
|
|
// Verify board post_count updated
|
|
got, _ := s.GetBoard(board.ID)
|
|
if got.PostCount != 2 {
|
|
t.Errorf("Board PostCount = %d, want 2", got.PostCount)
|
|
}
|
|
|
|
// List messages
|
|
msgs, err := s.ListMessages(board.ID, 0, 50)
|
|
if err != nil {
|
|
t.Fatalf("ListMessages: %v", err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Errorf("ListMessages returned %d, want 2", len(msgs))
|
|
}
|
|
if msgs[0].Title != "First Post" {
|
|
t.Errorf("First msg title = %q", msgs[0].Title)
|
|
}
|
|
if msgs[1].ReplyTo != msg1.ID {
|
|
t.Errorf("Reply message ReplyTo = %d, want %d", msgs[1].ReplyTo, msg1.ID)
|
|
}
|
|
|
|
// Count messages
|
|
count, _ := s.CountMessages(board.ID)
|
|
if count != 2 {
|
|
t.Errorf("CountMessages = %d, want 2", count)
|
|
}
|
|
|
|
// Delete message
|
|
if err := s.DeleteMessage(msg1.ID); err != nil {
|
|
t.Fatalf("DeleteMessage: %v", err)
|
|
}
|
|
count, _ = s.CountMessages(board.ID)
|
|
if count != 1 {
|
|
t.Errorf("CountMessages after delete = %d, want 1", count)
|
|
}
|
|
|
|
// Delete board (cascades to messages)
|
|
if err := s.DeleteBoard(board.ID); err != nil {
|
|
t.Fatalf("DeleteBoard: %v", err)
|
|
}
|
|
count, _ = s.CountMessages(board.ID)
|
|
if count != 0 {
|
|
t.Errorf("Messages should be cascade-deleted with board")
|
|
}
|
|
}
|
|
|
|
// --- Mail tests ---
|
|
|
|
func TestMail(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
mail := &models.Mail{
|
|
Title: "Hello",
|
|
Author: "Alice",
|
|
FromID: 1,
|
|
ToID: 2,
|
|
Recipient: "Bob",
|
|
Body: "How's it going?",
|
|
}
|
|
if err := s.CreateMail(mail); err != nil {
|
|
t.Fatalf("CreateMail: %v", err)
|
|
}
|
|
|
|
// Count unread
|
|
count, _ := s.CountUnreadMail(2)
|
|
if count != 1 {
|
|
t.Errorf("Unread mail for Bob = %d, want 1", count)
|
|
}
|
|
|
|
// List mail
|
|
mails, err := s.ListMailFor(2)
|
|
if err != nil {
|
|
t.Fatalf("ListMailFor: %v", err)
|
|
}
|
|
if len(mails) != 1 {
|
|
t.Fatalf("ListMailFor returned %d, want 1", len(mails))
|
|
}
|
|
if mails[0].Read {
|
|
t.Error("New mail should be unread")
|
|
}
|
|
|
|
// Mark read
|
|
if err := s.MarkMailRead(mail.ID); err != nil {
|
|
t.Fatalf("MarkMailRead: %v", err)
|
|
}
|
|
count, _ = s.CountUnreadMail(2)
|
|
if count != 0 {
|
|
t.Errorf("Unread after marking = %d, want 0", count)
|
|
}
|
|
|
|
// Delete
|
|
if err := s.DeleteMail(mail.ID); err != nil {
|
|
t.Fatalf("DeleteMail: %v", err)
|
|
}
|
|
got, _ := s.GetMail(mail.ID)
|
|
if got != nil {
|
|
t.Error("Deleted mail should be nil")
|
|
}
|
|
}
|
|
|
|
// --- Library tests ---
|
|
|
|
func TestLibraryAndFiles(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
lib := &models.Library{
|
|
Name: "Downloads",
|
|
FilePath: "/data/files/downloads",
|
|
UploadLow: 2,
|
|
UploadHigh: 255,
|
|
DownloadLow: 0,
|
|
DownloadHigh: 255,
|
|
MaxFiles: 50,
|
|
}
|
|
if err := s.CreateLibrary(lib); err != nil {
|
|
t.Fatalf("CreateLibrary: %v", err)
|
|
}
|
|
|
|
// Create file entry
|
|
file := &models.LibraryFile{
|
|
LibraryID: lib.ID,
|
|
Filename: "readme.txt",
|
|
Description: "Read this first",
|
|
UploaderID: 1,
|
|
Uploader: "Sysop",
|
|
FileSize: 1024,
|
|
}
|
|
if err := s.CreateLibraryFile(file); err != nil {
|
|
t.Fatalf("CreateLibraryFile: %v", err)
|
|
}
|
|
|
|
// Verify library file_count updated
|
|
got, _ := s.GetLibrary(lib.ID)
|
|
if got.FileCount != 1 {
|
|
t.Errorf("Library FileCount = %d, want 1", got.FileCount)
|
|
}
|
|
|
|
// List files
|
|
files, err := s.ListLibraryFiles(lib.ID, 0, 50)
|
|
if err != nil {
|
|
t.Fatalf("ListLibraryFiles: %v", err)
|
|
}
|
|
if len(files) != 1 {
|
|
t.Errorf("ListLibraryFiles returned %d, want 1", len(files))
|
|
}
|
|
|
|
// Delete file
|
|
if err := s.DeleteLibraryFile(file.ID); err != nil {
|
|
t.Fatalf("DeleteLibraryFile: %v", err)
|
|
}
|
|
got, _ = s.GetLibrary(lib.ID)
|
|
if got.FileCount != 0 {
|
|
t.Errorf("FileCount after delete = %d, want 0", got.FileCount)
|
|
}
|
|
}
|
|
|
|
// --- Bulletin tests ---
|
|
|
|
func TestBulletins(t *testing.T) {
|
|
s := testStore(t)
|
|
|
|
b := &models.Bulletin{
|
|
Name: "System Rules",
|
|
FilePath: "./screens/rules.ans",
|
|
ReadLow: 0,
|
|
ReadHigh: 255,
|
|
}
|
|
if err := s.CreateBulletin(b); err != nil {
|
|
t.Fatalf("CreateBulletin: %v", err)
|
|
}
|
|
|
|
bulletins, err := s.ListBulletins()
|
|
if err != nil {
|
|
t.Fatalf("ListBulletins: %v", err)
|
|
}
|
|
if len(bulletins) != 1 {
|
|
t.Errorf("ListBulletins returned %d, want 1", len(bulletins))
|
|
}
|
|
if bulletins[0].Name != "System Rules" {
|
|
t.Errorf("Name = %q, want %q", bulletins[0].Name, "System Rules")
|
|
}
|
|
|
|
if err := s.DeleteBulletin(b.ID); err != nil {
|
|
t.Fatalf("DeleteBulletin: %v", err)
|
|
}
|
|
bulletins, _ = s.ListBulletins()
|
|
if len(bulletins) != 0 {
|
|
t.Error("Bulletins should be empty after delete")
|
|
}
|
|
}
|
|
|
|
// --- Schema test ---
|
|
|
|
func TestOpenSQLiteCreatesDirectory(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "subdir", "deep", "test.db")
|
|
|
|
s, err := OpenSQLite(path)
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLite with nested path: %v", err)
|
|
}
|
|
s.Close()
|
|
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
t.Fatal("Database file was not created")
|
|
}
|
|
}
|
|
|
|
func timePtr(t time.Time) *time.Time { return &t }
|