Browse Source

more coverage

aeth 3 months ago
parent
commit
b5a88830e5

+ 1 - 1
Makefile

@@ -1,4 +1,4 @@
-.PHONY: build format docs
+.PHONY: build format test coverage dev-run install
 
 
 WEBSERVER = keiji

+ 2 - 2
cmd/keiji/keiji.go

@@ -93,8 +93,8 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
-	webserverDb := storage.NewSQLiteRepo(db)
-	err = webserverDb.Migrate()
+	webserverDb := storage.NewSQLiteRepo(db, storage.FilesystemImageIO{RootDir: os.Getenv(env.IMAGE_STORE)})
+	err = webserverDb.Migrate(storage.RequiredTables)
 	if err != nil {
 		log.Fatal(err)
 	}

+ 3 - 2
pkg/auth/auth.go

@@ -4,6 +4,7 @@ import (
 	"os"
 	"time"
 
+	"git.aetherial.dev/aeth/keiji/pkg/env"
 	"github.com/google/uuid"
 	"github.com/patrickmn/go-cache"
 )
@@ -51,8 +52,8 @@ type Source interface {
 
 type EnvAuth struct{}
 
-func (e EnvAuth) AdminUsername() string { return os.Getenv("KEIJI_USERNAME") }
-func (e EnvAuth) AdminPassword() string { return os.Getenv("KEIJI_PASSWORD") }
+func (e EnvAuth) AdminUsername() string { return os.Getenv(env.KEIJI_USERNAME) }
+func (e EnvAuth) AdminPassword() string { return os.Getenv(env.KEIJI_PASSWORD) }
 
 /*
 Recieve the credentials from frontend and validate them

+ 17 - 2
pkg/auth/auth_test.go

@@ -1,8 +1,10 @@
 package auth
 
 import (
+	"os"
 	"testing"
 
+	"git.aetherial.dev/aeth/keiji/pkg/env"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -64,8 +66,8 @@ func TestAuthorize(t *testing.T) {
 		{
 			desc:          "Auth fails because username is wrong",
 			inputUsername: "admin",
-			inputPassword: "",
-			realUsername:  "admin",
+			inputPassword: "abc123",
+			realUsername:  "superuser",
 			realPassword:  "abc123",
 			expectError:   &InvalidCredentials{},
 		},
@@ -81,3 +83,16 @@ func TestAuthorize(t *testing.T) {
 		})
 	}
 }
+
+func TestEnvAuth(t *testing.T) {
+	username := "testuser"
+	password := "testpass"
+	os.Setenv(env.KEIJI_USERNAME, username)
+	os.Setenv(env.KEIJI_PASSWORD, password)
+	defer os.Unsetenv(env.KEIJI_PASSWORD)
+	defer os.Unsetenv(env.KEIJI_PASSWORD)
+	authSrc := EnvAuth{}
+	assert.Equal(t, username, authSrc.AdminUsername())
+	assert.Equal(t, password, authSrc.AdminPassword())
+
+}

+ 2 - 2
pkg/controller/content_handlers.go

@@ -116,7 +116,7 @@ func (c *Controller) MakeBlogPost(ctx *gin.Context) {
 		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": "Update Failed!", "Color": "red"})
 		return
 	}
-	err = c.database.AddDocument(doc)
+	_, err = c.database.AddDocument(doc)
 	if err != nil {
 		ctx.HTML(400, "upload_status", gin.H{"UpdateMessage": "Update Failed!", "Color": "red"})
 		return
@@ -169,7 +169,7 @@ func (c *Controller) SaveFile(ctx *gin.Context) {
 		}
 		output.Write(fb[:n])
 	}
-	err = c.database.AddImage(fb, img.Title, img.Desc)
+	_, err = c.database.AddImage(fb, img.Title, img.Desc)
 	if err != nil {
 		ctx.HTML(500, "upload_status", gin.H{"UpdateMessage": err, "Color": "red"})
 		return

+ 53 - 0
pkg/storage/queries.go

@@ -0,0 +1,53 @@
+package storage
+
+const postsTable = `
+    CREATE TABLE IF NOT EXISTS posts(
+        row INTEGER PRIMARY KEY AUTOINCREMENT,
+		id TEXT NOT NULL UNIQUE,
+		title TEXT NOT NULL,
+        created TEXT NOT NULL,
+        body TEXT NOT NULL,
+        category TEXT NOT NULL,
+		sample TEXT NOT NULL
+    );
+    `
+const imagesTable = `
+	CREATE TABLE IF NOT EXISTS images(
+		row INTEGER PRIMARY KEY AUTOINCREMENT,
+		id TEXT NOT NULL,
+		title TEXT NOT NULL,
+		desc TEXT NOT NULL,
+		created TEXT NOT NULL
+	);
+	`
+const menuItemsTable = `
+	CREATE TABLE IF NOT EXISTS menu(
+		row INTEGER PRIMARY KEY AUTOINCREMENT,
+		link TEXT NOT NULL,
+		text TEXT NOT NULL
+	);
+	`
+const navbarItemsTable = `
+	CREATE TABLE IF NOT EXISTS navbar(
+		row INTEGER PRIMARY KEY AUTOINCREMENT,
+		png BLOB NOT NULL,
+		link TEXT NOT NULL,
+		redirect TEXT
+	);`
+const assetTable = `
+	CREATE TABLE IF NOT EXISTS assets(
+		row INTEGER PRIMARY KEY AUTOINCREMENT,
+		name TEXT NOT NULL,
+		data BLOB NOT NULL
+	);
+	`
+const adminTable = `
+	CREATE TABLE IF NOT EXISTS admin(
+		row INTEGER PRIMARY KEY AUTOINCREMENT,
+		display_name TEXT NOT NULL,
+		link TEXT NOT NULL,
+		category TEXT NOT NULL
+	);
+	`
+
+var RequiredTables = []string{postsTable, imagesTable, menuItemsTable, navbarItemsTable, assetTable, adminTable}

+ 79 - 78
pkg/storage/storage.go

@@ -4,6 +4,7 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
+	"io"
 	"log"
 	"mime/multipart"
 	"os"
@@ -112,8 +113,8 @@ type DocumentIO interface {
 	GetAllImages() []Image
 	UpdateDocument(doc Document) error
 	DeleteDocument(id Identifier) error
-	AddDocument(doc Document) error
-	AddImage(data []byte, title, desc string) error
+	AddDocument(doc Document) (Identifier, error)
+	AddImage(data []byte, title, desc string) (Identifier, error)
 	AddAsset(name string, data []byte) error
 	AddAdminTableEntry(TableData, string) error
 	AddNavbarItem(NavBarItem) error
@@ -134,70 +135,65 @@ var (
 )
 
 type SQLiteRepo struct {
-	db *sql.DB
+	db      *sql.DB
+	imageIO ImageIO
+}
+
+type ImageIO interface {
+	Put([]byte, Identifier) error
+	Get(Identifier) ([]byte, error)
+}
+
+type FilesystemImageIO struct {
+	RootDir string
+}
+
+/*
+Put a data blob on the filesystem
+
+	:param b: the
+*/
+func (f FilesystemImageIO) Put(b []byte, id Identifier) error {
+	fh, err := os.OpenFile(path.Join(f.RootDir, string(id)), os.O_CREATE|os.O_RDWR, os.ModePerm)
+	if err != nil {
+		return err
+	}
+	defer fh.Close()
+	_, err = fh.Write(b)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+/*
+Get a data blob from the filesystem
+
+	:param id: the identifier of the image to retrieve
+*/
+func (f FilesystemImageIO) Get(id Identifier) ([]byte, error) {
+	fh, err := os.Open(path.Join(f.RootDir, string(id)))
+	if err != nil {
+		return nil, err
+	}
+	b, err := io.ReadAll(fh)
+	if err != nil {
+		return nil, err
+	}
+	return b, nil
 }
 
 // Instantiate a new SQLiteRepo struct
-func NewSQLiteRepo(db *sql.DB) *SQLiteRepo {
+func NewSQLiteRepo(db *sql.DB, imgIo ImageIO) *SQLiteRepo {
 	return &SQLiteRepo{
-		db: db,
+		db:      db,
+		imageIO: imgIo,
 	}
 
 }
 
 // Creates a new SQL table for text posts
-func (r *SQLiteRepo) Migrate() error {
-	postsTable := `
-    CREATE TABLE IF NOT EXISTS posts(
-        row INTEGER PRIMARY KEY AUTOINCREMENT,
-		id TEXT NOT NULL UNIQUE,
-		title TEXT NOT NULL,
-        created TEXT NOT NULL,
-        body TEXT NOT NULL,
-        category TEXT NOT NULL,
-		sample TEXT NOT NULL
-    );
-    `
-	imagesTable := `
-	CREATE TABLE IF NOT EXISTS images(
-		row INTEGER PRIMARY KEY AUTOINCREMENT,
-		id TEXT NOT NULL,
-		title TEXT NOT NULL,
-		location TEXT NOT NULL,
-		desc TEXT NOT NULL,
-		created TEXT NOT NULL
-	);
-	`
-	menuItemsTable := `
-	CREATE TABLE IF NOT EXISTS menu(
-		row INTEGER PRIMARY KEY AUTOINCREMENT,
-		link TEXT NOT NULL,
-		text TEXT NOT NULL
-	);
-	`
-	navbarItemsTable := `
-	CREATE TABLE IF NOT EXISTS navbar(
-		row INTEGER PRIMARY KEY AUTOINCREMENT,
-		png BLOB NOT NULL,
-		link TEXT NOT NULL,
-		redirect TEXT
-	);`
-	assetTable := `
-	CREATE TABLE IF NOT EXISTS assets(
-		row INTEGER PRIMARY KEY AUTOINCREMENT,
-		name TEXT NOT NULL,
-		data BLOB NOT NULL
-	);
-	`
-	adminTable := `
-	CREATE TABLE IF NOT EXISTS admin(
-		row INTEGER PRIMARY KEY AUTOINCREMENT,
-		display_name TEXT NOT NULL,
-		link TEXT NOT NULL,
-		category TEXT NOT NULL
-	);
-	`
-	seedQueries := []string{postsTable, imagesTable, menuItemsTable, navbarItemsTable, assetTable, adminTable}
+func (r *SQLiteRepo) Migrate(seedQueries []string) error {
 	for i := range seedQueries {
 		_, err := r.db.Exec(seedQueries[i])
 		if err != nil {
@@ -345,18 +341,18 @@ get image data from the images table
 func (s *SQLiteRepo) GetImage(id Identifier) (Image, error) {
 	row := s.db.QueryRow("SELECT * FROM images WHERE id = ?", id)
 	var rowNum int
-	var title, location, desc, created string
-	if err := row.Scan(&rowNum, &id, &title, &location, &desc, &created); err != nil {
+	var title, desc, created string
+	if err := row.Scan(&rowNum, &id, &title, &desc, &created); err != nil {
 		if errors.Is(err, sql.ErrNoRows) {
 			return Image{}, ErrNotExists
 		}
 		return Image{}, err
 	}
-	data, err := os.ReadFile(location)
+	data, err := s.imageIO.Get(id)
 	if err != nil {
 		return Image{}, err
 	}
-	return Image{Ident: id, Title: title, Location: location, Desc: desc, Data: data, Created: created}, nil
+	return Image{Ident: id, Title: title, Desc: desc, Data: data, Created: created}, nil
 }
 
 /*
@@ -371,15 +367,15 @@ func (s *SQLiteRepo) GetAllImages() []Image {
 	for rows.Next() {
 		var img Image
 		var rowNum int
-		err := rows.Scan(&rowNum, &img.Ident, &img.Title, &img.Location, &img.Desc, &img.Created)
+		err := rows.Scan(&rowNum, &img.Ident, &img.Title, &img.Desc, &img.Created)
 		if err != nil {
 			log.Fatal(err)
 		}
-		b, err := os.ReadFile(img.Location)
+		b, err := s.imageIO.Get(img.Ident)
 		if err != nil {
 			log.Fatal(err)
 		}
-		imgs = append(imgs, Image{Ident: img.Ident, Title: img.Title, Location: img.Location, Desc: img.Desc, Data: b, Created: img.Created})
+		imgs = append(imgs, Image{Ident: img.Ident, Title: img.Title, Desc: img.Desc, Data: b, Created: img.Created})
 	}
 	err = rows.Err()
 	if err != nil {
@@ -396,18 +392,17 @@ Add an image to the database
 	:param desc: the description of the image, if any
 	:param data: the binary data for the image
 */
-func (s *SQLiteRepo) AddImage(data []byte, title string, desc string) error {
+func (s *SQLiteRepo) AddImage(data []byte, title string, desc string) (Identifier, error) {
 	id := newIdentifier()
-	fsLoc := path.Join(GetImageStore(), string(id))
-	err := os.WriteFile(fsLoc, data, os.ModePerm)
+	err := s.imageIO.Put(data, id)
 	if err != nil {
-		return err
+		return Identifier(""), err
 	}
-	_, err = s.db.Exec("INSERT INTO images (id, title, location, desc, created) VALUES (?,?,?,?,?)", string(id), title, fsLoc, desc, time.Now().String())
+	_, err = s.db.Exec("INSERT INTO images (id, title, desc, created) VALUES (?,?,?,?)", string(id), title, desc, time.Now().String())
 	if err != nil {
-		return err
+		return Identifier(""), err
 	}
-	return nil
+	return id, nil
 }
 
 /*
@@ -425,11 +420,17 @@ func (s *SQLiteRepo) UpdateDocument(doc Document) error {
 		tx.Rollback()
 		return err
 	}
-	_, err = stmt.Exec(doc.Title, doc.Body, doc.Category, doc.MakeSample(), doc.Ident)
+
+	res, err := stmt.Exec(doc.Title, doc.Body, doc.Category, doc.MakeSample(), doc.Ident)
 	if err != nil {
 		tx.Rollback()
 		return err
 	}
+	affected, _ := res.RowsAffected()
+	if affected != 1 {
+		return ErrNotExists
+	}
+
 	tx.Commit()
 	return nil
 }
@@ -506,20 +507,20 @@ Adds a document to the database (for text posts)
 
 	:param doc: the Document to add
 */
-func (s *SQLiteRepo) AddDocument(doc Document) error {
-	id := uuid.New()
+func (s *SQLiteRepo) AddDocument(doc Document) (Identifier, error) {
+	id := newIdentifier()
 	tx, err := s.db.Begin()
 	if err != nil {
-		return err
+		return Identifier(""), err
 	}
 	stmt, _ := tx.Prepare("INSERT INTO posts(id, title, created, body, category, sample) VALUES (?,?,?,?,?,?)")
-	_, err = stmt.Exec(id.String(), doc.Title, doc.Created, doc.Body, doc.Category, doc.MakeSample())
+	_, err = stmt.Exec(id, doc.Title, doc.Created, doc.Body, doc.Category, doc.MakeSample())
 	if err != nil {
 		tx.Rollback()
-		return err
+		return Identifier(""), err
 	}
 	tx.Commit()
-	return nil
+	return id, nil
 
 }
 

+ 437 - 55
pkg/storage/storage_test.go

@@ -2,23 +2,64 @@ package storage
 
 import (
 	"database/sql"
+	"errors"
 	"log"
-	"os"
-	"path"
 	"testing"
 
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/assert"
 )
 
-func newTestDb() (*SQLiteRepo, *sql.DB) {
+const badPostsTable = `
+    CREATE TABLE IF NOT EXISTS posts(
+        row INTEGER PRIMARY KEY AUTOINCREMENT
+    );
+    `
+const badImagesTable = `
+	CREATE TABLE IF NOT EXISTS images(
+		row INTEGER PRIMARY KEY AUTOINCREMENT
+	);
+	`
+const badMenuItemsTable = `
+	CREATE TABLE IF NOT EXISTS menu(
+		row INTEGER PRIMARY KEY AUTOINCREMENT
+	);
+	`
+const badNavbarItemsTable = `
+	CREATE TABLE IF NOT EXISTS navbar(
+		row INTEGER PRIMARY KEY AUTOINCREMENT
+	);`
+const badAssetTable = `
+	CREATE TABLE IF NOT EXISTS assets(
+		row INTEGER PRIMARY KEY AUTOINCREMENT
+	);
+	`
+const badAdminTable = `
+	CREATE TABLE IF NOT EXISTS admin(
+		row INTEGER PRIMARY KEY AUTOINCREMENT
+	);
+	`
+
+var unpopulatedTables = []string{badPostsTable, badImagesTable, badMenuItemsTable, badMenuItemsTable, badAssetTable, badAdminTable}
+
+/*
+creates in memory db and SQLiteRepo struct
+
+	:param tmp: path to the temp directory for the filesystem IO struct to write images to
+	:param migrate: choose to 'migrate' the database and create all the tables
+*/
+func newTestDb(tmp string, migrate bool) (*SQLiteRepo, *sql.DB) {
 
 	db, err := sql.Open("sqlite3", ":memory:")
 	if err != nil {
 		log.Fatal(err)
 	}
-	testDb := &SQLiteRepo{db: db}
-	err = testDb.Migrate()
+	testDb := &SQLiteRepo{db: db, imageIO: FilesystemImageIO{RootDir: tmp}}
+	if migrate {
+		err = testDb.Migrate(RequiredTables)
+	} else {
+		err = testDb.Migrate(unpopulatedTables)
+	}
 	if err != nil {
 		log.Fatal("failed to start the test database: ", err)
 	}
@@ -40,7 +81,7 @@ func TestMigrate(t *testing.T) {
 		log.Fatal(err)
 	}
 	testDb := &SQLiteRepo{db: db}
-	err = testDb.Migrate()
+	err = testDb.Migrate(RequiredTables)
 	if err != nil {
 		t.Error(err)
 	}
@@ -61,7 +102,7 @@ func TestGetDropdownElements(t *testing.T) {
 	type testcase struct {
 		seed []LinkPair
 	}
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			seed: []LinkPair{
@@ -89,7 +130,7 @@ func TestGetNavBarLinks(t *testing.T) {
 	type testcase struct {
 		seed []NavBarItem
 	}
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			seed: []NavBarItem{
@@ -117,7 +158,7 @@ func TestGetAssets(t *testing.T) {
 	type testcase struct {
 		seed []Asset
 	}
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			seed: []Asset{
@@ -146,7 +187,7 @@ func TestGetAdminTables(t *testing.T) {
 	type testcase struct {
 		seed AdminPage
 	}
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			seed: AdminPage{
@@ -181,7 +222,7 @@ func TestGetDocument(t *testing.T) {
 	type testcase struct {
 		seed Document
 	}
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			seed: Document{
@@ -209,7 +250,7 @@ func TestGetByCategory(t *testing.T) {
 	type testcase struct {
 		seed []Document
 	}
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	for _, tc := range []testcase{
 		{
 			seed: []Document{
@@ -248,71 +289,79 @@ func TestGetByCategory(t *testing.T) {
 }
 func TestGetImage(t *testing.T) {
 
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	type testcase struct {
-		seed Image
+		seed       Image
+		shouldSeed bool
+		err        error
 	}
-	tmp := t.TempDir()
-	testImageLoc := path.Join(tmp, "test.jpg")
 	for _, tc := range []testcase{
 		{
 			seed: Image{
-				Ident:    Identifier("abc123"),
-				Title:    "xyz098",
-				Location: testImageLoc,
-				Desc:     "description",
-				Created:  "2024-12-31",
-				Data:     []byte("abc123xyz098"),
+				Ident:   Identifier("abc123"),
+				Title:   "xyz098",
+				Desc:    "description",
+				Created: "2024-12-31",
+				Data:    []byte("abc123xyz098"),
+			},
+			shouldSeed: true,
+			err:        nil,
+		},
+		{
+			seed: Image{
+				Ident: Identifier("zxcvbnm"),
 			},
+			shouldSeed: false,
+			err:        ErrNotExists,
 		},
 	} {
-		_, err := db.Exec("INSERT INTO images (id, title, location, desc, created) VALUES (?,?,?,?,?)", string(tc.seed.Ident), tc.seed.Title, tc.seed.Location, tc.seed.Desc, "2024-12-31")
-		if err != nil {
-			t.Error(err)
+		if tc.shouldSeed {
+			_, err := db.Exec("INSERT INTO images (id, title, desc, created) VALUES (?,?,?,?)", string(tc.seed.Ident), tc.seed.Title, tc.seed.Desc, "2024-12-31")
+			if err != nil {
+				t.Error(err)
+			}
+			testDb.imageIO.Put(tc.seed.Data, tc.seed.Ident)
 		}
-		os.WriteFile(tc.seed.Location, tc.seed.Data, os.ModePerm)
 		got, err := testDb.GetImage(tc.seed.Ident)
 		if err != nil {
-			t.Error(err)
+			assert.Equal(t, tc.err, err)
+		} else {
+			assert.Equal(t, tc.seed, got)
 		}
-		assert.Equal(t, tc.seed, got)
 	}
 }
 func TestGetAllImages(t *testing.T) {
 
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 	type testcase struct {
 		seed []Image
 	}
-	tmp := t.TempDir()
 	for _, tc := range []testcase{
 		{
 			seed: []Image{
 				{
-					Ident:    Identifier("abc123"),
-					Title:    "xyz098",
-					Location: path.Join(tmp, "1.jpg"),
-					Data:     []byte("abc123xyz098"),
-					Created:  "2024-12-31",
-					Desc:     "description",
+					Ident:   Identifier("abc123"),
+					Title:   "xyz098",
+					Data:    []byte("abc123xyz098"),
+					Created: "2024-12-31",
+					Desc:    "description",
 				},
 				{
-					Ident:    Identifier("xyz098"),
-					Title:    "abc123",
-					Location: path.Join(tmp, "2.jpg"),
-					Data:     []byte("abc123xyz098"),
-					Created:  "2024-12-31",
-					Desc:     "description",
+					Ident:   Identifier("xyz098"),
+					Title:   "abc123",
+					Data:    []byte("abc123xyz098"),
+					Created: "2024-12-31",
+					Desc:    "description",
 				},
 			},
 		},
 	} {
 		for i := range tc.seed {
-			_, err := db.Exec("INSERT INTO images (id, title, location, desc, created) VALUES (?,?,?,?,?)", string(tc.seed[i].Ident), tc.seed[i].Title, tc.seed[i].Location, tc.seed[i].Desc, tc.seed[i].Created)
+			_, err := db.Exec("INSERT INTO images (id, title, desc, created) VALUES (?,?,?,?)", string(tc.seed[i].Ident), tc.seed[i].Title, tc.seed[i].Desc, tc.seed[i].Created)
 			if err != nil {
 				t.Error(err)
 			}
-			os.WriteFile(tc.seed[i].Location, tc.seed[i].Data, os.ModePerm)
+			testDb.imageIO.Put(tc.seed[i].Data, tc.seed[i].Ident)
 		}
 		got := testDb.GetAllImages()
 		assert.Equal(t, tc.seed, got)
@@ -321,7 +370,7 @@ func TestGetAllImages(t *testing.T) {
 }
 
 func TestAllDocuments(t *testing.T) {
-	testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
 
 	type testcase struct {
 		seed []Document
@@ -364,45 +413,378 @@ func TestAllDocuments(t *testing.T) {
 }
 
 func TestUpdateDocument(t *testing.T) {
+	type testcase struct {
+		migrate bool
+		seed    Document
+		input   Document
+		err     error
+	}
 
-	// testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			migrate: true,
+			seed: Document{
+				Row:      1,
+				Ident:    Identifier("qwerty"),
+				Title:    "abc 123",
+				Created:  "2024-12-31",
+				Body:     "blog post body etc",
+				Category: BLOG,
+				Sample:   "this is a sample",
+			},
+			input: Document{
+				Row:      1,
+				Ident:    Identifier("qwerty"),
+				Title:    "new title",
+				Created:  "2024-12-31",
+				Body:     "new updated post that must be reflected after the update",
+				Category: BLOG,
+				Sample:   "new updated post that must be reflected after the update",
+			},
+			err: nil,
+		},
+		{
+			migrate: true,
+			seed: Document{
+				Row:      1,
+				Ident:    Identifier("asdf"),
+				Title:    "abc 123",
+				Created:  "2024-12-31",
+				Body:     "blog post body etc",
+				Category: BLOG,
+				Sample:   "this is a sample",
+			},
+			input: Document{
+				Row:      1,
+				Ident:    Identifier("This id does not exist"),
+				Title:    "new title",
+				Created:  "2024-12-31",
+				Body:     "new updated post that must be reflected after the update",
+				Category: BLOG,
+				Sample:   "new updated post that must be reflected after the update",
+			},
+			err: ErrNotExists,
+		},
+		{
+			migrate: false, // not creating the database tables so we can error out the SQL statement execution
+			seed: Document{
+				Row:      1,
+				Ident:    Identifier("asdf"),
+				Title:    "abc 123",
+				Created:  "2024-12-31",
+				Body:     "blog post body etc",
+				Category: BLOG,
+				Sample:   "this is a sample",
+			},
+			input: Document{
+				Row:      1,
+				Ident:    Identifier("This id does not exist"),
+				Title:    "new title",
+				Created:  "2024-12-31",
+				Body:     "new updated post that must be reflected after the update",
+				Category: BLOG,
+				Sample:   "new updated post that must be reflected after the update",
+			},
+			err: errors.New("no such column: title"),
+		},
+	} {
+		testDb, db := newTestDb(t.TempDir(), tc.migrate)
+		if tc.migrate {
+			stmt, _ := db.Prepare("INSERT INTO posts(id, title, created, body, category, sample) VALUES (?,?,?,?,?,?)")
+			_, err := stmt.Exec(tc.seed.Ident, tc.seed.Title, tc.seed.Created, tc.seed.Body, tc.seed.Category, tc.seed.Sample)
+			if err != nil {
+				t.Error(err)
+			}
+		}
+		err := testDb.UpdateDocument(tc.input)
+		if err != nil {
+			assert.Equal(t, tc.err.Error(), err.Error())
+		} else {
+
+			row := db.QueryRow("SELECT * FROM posts WHERE id = ?", tc.seed.Ident)
+			var got Document
+			if err := row.Scan(&got.Row, &got.Ident, &got.Title, &got.Created, &got.Body, &got.Category, &got.Sample); err != nil {
+				assert.Equal(t, tc.err, err)
+			}
+			assert.Equal(t, tc.input, got)
+		}
+
+	}
 }
 
 func TestAddImage(t *testing.T) {
 
-	// testDb, db := newTestDb()
+	type testcase struct {
+		data  []byte
+		title string
+		desc  string
+		err   error
+	}
+	testDb, _ := newTestDb(t.TempDir(), true)
+	for _, tc := range []testcase{
+		{
+			data:  []byte("abc123xyz098"),
+			title: "dont matter",
+			desc:  "also dont matter",
+		},
+	} {
+		id, err := testDb.AddImage(tc.data, tc.title, tc.desc)
+		if err != nil {
+			assert.Equal(t, tc.err, err)
+		} else {
+			b, err := testDb.imageIO.Get(id)
+			if err != nil {
+				t.Error(err)
+			}
+			assert.Equal(t, tc.data, b)
+		}
+
+	}
+
 }
 func TestAddMenuItem(t *testing.T) {
 
-	// testDb, db := newTestDb()
+	type testcase struct {
+		input []LinkPair
+		err   error
+	}
+	testDb, db := newTestDb(t.TempDir(), true)
+	for _, tc := range []testcase{
+		{
+			input: []LinkPair{
+				{
+					Text: "abc 123",
+					Link: "/abc/123",
+				},
+			},
+			err: nil,
+		},
+	} {
+		for i := range tc.input {
+			err := testDb.AddMenuItem(tc.input[i])
+			if err != nil {
+				assert.Equal(t, tc.err, err)
+			}
+			rows, err := db.Query("SELECT * FROM menu")
+			var got []LinkPair
+			defer rows.Close()
+			for rows.Next() {
+				var id int
+				var item LinkPair
+				err = rows.Scan(&id, &item.Link, &item.Text)
+				if err != nil {
+					log.Fatal(err)
+				}
+				got = append(got, item)
+			}
+			assert.Equal(t, tc.input, got)
+		}
+	}
 }
 func TestAddNavbarItem(t *testing.T) {
+	type testcase struct {
+		input []NavBarItem
+		err   error
+	}
+	testDb, db := newTestDb(t.TempDir(), true)
+	for _, tc := range []testcase{
+		{
+			input: []NavBarItem{
+				{
+					Redirect: "",
+					Link:     "",
+					Png:      []byte(""),
+				},
+			},
+		},
+	} {
+		for i := range tc.input {
+			err := testDb.AddNavbarItem(tc.input[i])
+			if err != nil {
+				assert.Equal(t, tc.err, err)
+			}
+
+		}
+		rows, err := db.Query("SELECT * FROM navbar")
+		var got []NavBarItem
+		defer rows.Close()
+		for rows.Next() {
+			var item NavBarItem
+			var id int
+			err = rows.Scan(&id, &item.Png, &item.Link, &item.Redirect)
+			if err != nil {
+				log.Fatal(err)
+			}
+			got = append(got, item)
+		}
+		assert.Equal(t, tc.input, got)
 
-	// testDb, db := newTestDb()
+	}
 }
 func TestAddAsset(t *testing.T) {
+	type testcase struct {
+		input []Asset
+		err   error
+	}
+	testDb, db := newTestDb(t.TempDir(), true)
+	for _, tc := range []testcase{
+		{
+			input: []Asset{
+				{
+					Data: []byte(""),
+					Name: "",
+				},
+			},
+		},
+	} {
+		for i := range tc.input {
+			err := testDb.AddAsset(tc.input[i].Name, tc.input[i].Data)
+			if err != nil {
+				assert.Equal(t, tc.err, err)
+			}
 
-	// testDb, db := newTestDb()
+		}
+		rows, err := db.Query("SELECT * FROM assets")
+		var assets []Asset
+		defer rows.Close()
+		for rows.Next() {
+			var item Asset
+			var id int
+			err = rows.Scan(&id, &item.Name, &item.Data)
+			if err != nil {
+				log.Fatal(err)
+			}
+			assets = append(assets, item)
+		}
+	}
 }
 func TestAddDocument(t *testing.T) {
 
-	// testDb, db := newTestDb()
+	testDb, db := newTestDb(t.TempDir(), true)
+	type testcase struct {
+		seed Document
+		err  error
+	}
+	for _, tc := range []testcase{
+		{
+			seed: Document{
+				Title:    "abc 123",
+				Body:     "blog post body etc",
+				Created:  "2024-12-31",
+				Category: BLOG,
+				Sample:   "this is a sample",
+			},
+			err: nil,
+		},
+	} {
+		id, err := testDb.AddDocument(tc.seed)
+		if err != nil {
+			assert.Equal(t, tc.err, err)
+		}
+		row := db.QueryRow("SELECT * FROM posts WHERE id = ?", id)
+		var got Document
+		var rowNum int
+		if err := row.Scan(&rowNum, &got.Ident, &got.Title, &got.Created, &got.Body, &got.Category, &got.Sample); err != nil {
+			assert.Equal(t, tc.err, err)
+		}
+		want := Document{
+			Ident:    id,
+			Title:    tc.seed.Title,
+			Body:     tc.seed.Body,
+			Category: tc.seed.Category,
+			Created:  tc.seed.Created,
+			Sample:   tc.seed.MakeSample(),
+		}
+
+		assert.Equal(t, want, got)
+	}
 }
 func TestAddAdminTableEntry(t *testing.T) {
+	type testcase struct {
+		input AdminPage
+		err   error
+	}
+	testDb, db := newTestDb(t.TempDir(), true)
+	for _, tc := range []testcase{
+		{
+			input: AdminPage{
+				Tables: map[string][]TableData{
+					"test category": {
+						{
+							DisplayName: "abc 123",
+							Link:        "/abc/123",
+						},
+					},
+				},
+			},
+			err: nil,
+		},
+	} {
+		for ctg, tables := range tc.input.Tables {
+			for i := range tables {
+				err := testDb.AddAdminTableEntry(tables[i], ctg)
+				if err != nil {
+					assert.Equal(t, tc.err, err)
+				}
+			}
+		}
+		rows, err := db.Query("SELECT * FROM admin")
+		got := AdminPage{Tables: map[string][]TableData{}}
+		defer rows.Close()
+		for rows.Next() {
+			var item TableData
+			var id int
+			var category string
+			err = rows.Scan(&id, &item.DisplayName, &item.Link, &category)
+			if err != nil {
+				log.Fatal(err)
+			}
+			got.Tables[category] = append(got.Tables[category], item)
 
-	// testDb, db := newTestDb()
+		}
+		assert.Equal(t, tc.input, got)
+	}
 }
 
 func TestDeleteDocument(t *testing.T) {
+	type testcase struct {
+		input Document
+		err   error
+	}
+	testDb, db := newTestDb(t.TempDir(), true)
+	for _, tc := range []testcase{
+		{
+			input: Document{
+				Title:    "abc 123",
+				Body:     "blog post body etc",
+				Created:  "2024-12-31",
+				Category: BLOG,
+				Sample:   "this is a sample",
+			},
+			err: nil,
+		},
+	} {
+		id, err := testDb.AddDocument(tc.input)
+		if err != nil {
+			t.Error("failed to add doc: ", err)
+		}
+		err = testDb.DeleteDocument(id)
+		if err != nil {
+			assert.Equal(t, tc.err, err)
+		}
+		row, _ := db.Query("SELECT * FROM posts")
+		if row.Next() {
+			t.Error("Too many rows returned after deleting")
+		}
 
-	// testDb, db := newTestDb()
+	}
 }
 
 func TestGetImageStore(t *testing.T) {
 
-	// testDb, db := newTestDb()
+	// testDb, db := newTestDb(t.TempDir(), true)
 }
 func TestNewIdentifier(t *testing.T) {
 
-	// testDb, db := newTestDb()
+	// testDb, db := newTestDb(t.TempDir(), true)
 }

+ 5 - 3
pkg/webpages/pages.go

@@ -9,6 +9,8 @@ import (
 	"log"
 	"os"
 	"path"
+
+	"git.aetherial.dev/aeth/keiji/pkg/env"
 )
 
 //go:embed html cdn
@@ -32,7 +34,7 @@ func NewContentLayer(opt ServiceOption) fs.FS {
 	if opt == FILESYSTEM {
 		fmt.Println("Using filesystem to pull html templates")
 
-		return FilesystemWebpages{Webroot: path.Base(os.Getenv("WEB_ROOT"))}
+		return FilesystemWebpages{Webroot: path.Base(os.Getenv(env.WEB_ROOT))}
 	}
 	log.Fatal("Unknown option was passed: ", opt)
 	return content
@@ -51,11 +53,11 @@ type FilesystemWebpages struct {
 Implementing the io.FS interface for interoperability
 */
 func (f FilesystemWebpages) Open(file string) (fs.File, error) {
-	filePath := path.Join(os.Getenv("WEB_ROOT"), file)
+	filePath := path.Join(f.Webroot, file)
 	fh, err := os.Open(filePath)
 	if err != nil {
 		fmt.Printf("Error opening the file: %s because %s", filePath, err)
-		return nil, err
+		return nil, os.ErrNotExist
 	}
 	return fh, nil
 }

+ 83 - 0
pkg/webpages/pages_test.go

@@ -1 +1,84 @@
 package webpages
+
+import (
+	"io"
+	"io/fs"
+	"os"
+	"path"
+	"testing"
+
+	"git.aetherial.dev/aeth/keiji/pkg/env"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewContentLayer(t *testing.T) {
+	type testcase struct {
+		input ServiceOption
+		want  fs.FS
+	}
+	for _, tc := range []testcase{
+		{
+			input: EMBED,
+			want:  content,
+		},
+		{
+			input: FILESYSTEM,
+			want:  FilesystemWebpages{Webroot: path.Base(os.Getenv(env.WEB_ROOT))},
+		},
+	} {
+		got := NewContentLayer(tc.input)
+		assert.Equal(t, tc.want, got)
+
+	}
+}
+
+func TestOpen(t *testing.T) {
+
+	type testcase struct {
+		input      string
+		dir        string
+		createFile string
+		data       []byte
+		err        error
+	}
+	for _, tc := range []testcase{
+		{
+			input:      "testfile.txt",
+			dir:        t.TempDir(),
+			createFile: "testfile.txt",
+			data:       []byte("testdataetcabc123"),
+			err:        nil,
+		},
+		{
+			input:      "this_cant_be_indexed.csv",
+			dir:        t.TempDir(),
+			createFile: "test",
+			data:       []byte("testdatagdfsagfdsbhs"),
+			err:        os.ErrNotExist,
+		},
+	} {
+		os.WriteFile(path.Join(tc.dir, tc.createFile), tc.data, os.ModePerm)
+		testFs := FilesystemWebpages{Webroot: tc.dir}
+		fh, err := testFs.Open(tc.input)
+		if err != nil {
+			assert.Equal(t, tc.err, err)
+		} else {
+			b, _ := io.ReadAll(fh)
+			assert.Equal(t, tc.data, b)
+		}
+
+	}
+
+}
+
+func TestReadToString(t *testing.T) {
+
+	testFs := FilesystemWebpages{Webroot: t.TempDir()}
+	filename := "testfile.txt"
+	testFile := path.Join(testFs.Webroot, filename)
+	data := []byte("abc123xyz098")
+	os.WriteFile(testFile, data, os.ModePerm)
+	got := ReadToString(testFs, filename)
+	assert.Equal(t, string(data), got)
+
+}