Prechádzať zdrojové kódy

finally forced myself to write unit tests :(

aeth 2 mesiacov pred
rodič
commit
29edbb58d9

+ 4 - 0
.gitignore

@@ -14,3 +14,7 @@ sqlite.db
 # ignoring nohup.out file
 nohup.out
 
+coverage/
+
+
+

+ 10 - 9
Makefile

@@ -6,8 +6,7 @@ SEED_CMD = keiji-ctl
 SWAG := $(shell command -v swag 2> /dev/null)
 ## Have to set the WEB_ROOT and DOMAIN_NAME environment variables when building
 build:
-	go build -ldflags " -X main.DOMAIN_NAME=$(DOMAIN_NAME)" \
-	-o ./build/linux/$(WEBSERVER)/$(WEBSERVER) ./cmd/$(WEBSERVER)/$(WEBSERVER).go && \
+	go build -o ./build/linux/$(WEBSERVER)/$(WEBSERVER) ./cmd/$(WEBSERVER)/$(WEBSERVER).go && \
 	go build -o ./build/linux/$(SEED_CMD)/$(SEED_CMD) ./cmd/$(SEED_CMD)/$(SEED_CMD).go
 
 install:
@@ -16,13 +15,15 @@ install:
 format:
 	go fmt ./...
 
-docs:
-ifndef SWAG
-	$(error "Could not find the swag binary.")
-endif
-	swag init -g ./cmd/$(WEBSERVER)/$(WEBSERVER).go
+test:
+	go test ./...
+
+
+coverage:
+	go test -v ./... -covermode=count -coverpkg=./... -coverprofile coverage/coverage.out
+	go tool cover -html coverage/coverage.out -o coverage/coverage.html
+
 
 dev-run:
-	go build -ldflags "-X main.WEB_ROOT=$(WEB_ROOT)" \
-	-o ./build/linux/$(WEBSERVER)/$(WEBSERVER) ./cmd/$(WEBSERVER)/$(WEBSERVER).go && \
+	go build -o ./build/linux/$(WEBSERVER)/$(WEBSERVER) ./cmd/$(WEBSERVER)/$(WEBSERVER).go && \
 	./build/linux/$(WEBSERVER)/$(WEBSERVER) .env

+ 0 - 0
README.md


+ 8 - 2
cmd/keiji/keiji.go

@@ -13,6 +13,7 @@ import (
 	"github.com/gin-contrib/multitemplate"
 	"github.com/gin-gonic/gin"
 
+	"git.aetherial.dev/aeth/keiji/pkg/auth"
 	"git.aetherial.dev/aeth/keiji/pkg/env"
 	"git.aetherial.dev/aeth/keiji/pkg/routes"
 	"git.aetherial.dev/aeth/keiji/pkg/storage"
@@ -34,7 +35,12 @@ func main() {
 	flag.BoolVar(&blank, "blank", false, "create a blank .env template")
 	flag.Parse()
 	if blank {
-		env.WriteTemplate(".env.template")
+		fh, err := os.OpenFile(".env.template", os.O_CREATE|os.O_RDWR, os.ModePerm)
+		if err != nil {
+			log.Fatal("Couldnt open file .env.template, error: ", err)
+		}
+		defer fh.Close()
+		env.WriteTemplate(fh)
 		fmt.Println("Blank template written to: .env.template")
 		os.Exit(0)
 	}
@@ -92,7 +98,7 @@ func main() {
 	if err != nil {
 		log.Fatal(err)
 	}
-	routes.Register(e, os.Getenv("DOMAIN_NAME"), webserverDb, htmlReader)
+	routes.Register(e, os.Getenv("DOMAIN_NAME"), webserverDb, htmlReader, auth.EnvAuth{})
 	ssl, err := strconv.ParseBool(os.Getenv("USE_SSL"))
 	if err != nil {
 		log.Fatal("Invalid option passed to USE_SSL: ", os.Getenv("USE_SSL"))

+ 3 - 0
go.mod

@@ -10,6 +10,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
 	github.com/chenzhuoyu/iasm v0.9.1 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/gin-contrib/multitemplate v0.0.0-20231230012943-32b233489a81 // indirect
@@ -37,7 +38,9 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/redis/go-redis/v9 v9.4.0 // indirect
+	github.com/stretchr/testify v1.10.0 // indirect
 	github.com/swaggo/files v1.0.1 // indirect
 	github.com/swaggo/gin-swagger v1.6.0 // indirect
 	github.com/swaggo/swag v1.16.2 // indirect

+ 4 - 0
go.sum

@@ -19,6 +19,7 @@ github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0
 github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@@ -103,6 +104,7 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
 github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
 github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
 github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
@@ -119,6 +121,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
 github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
 github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=

+ 13 - 3
pkg/auth/auth.go

@@ -44,17 +44,27 @@ func (c *AuthCache) Read(id string) bool {
 	return ok
 }
 
+type Source interface {
+	AdminUsername() string
+	AdminPassword() string
+}
+
+type EnvAuth struct{}
+
+func (e EnvAuth) AdminUsername() string { return os.Getenv("KEIJI_USERNAME") }
+func (e EnvAuth) AdminPassword() string { return os.Getenv("KEIJI_PASSWORD") }
+
 /*
 Recieve the credentials from frontend and validate them
 
 	:param c: pointer to Credential struct
 */
-func Authorize(c *Credentials, cache *AuthCache) (string, error) {
+func Authorize(c *Credentials, cache *AuthCache, authSrc Source) (string, error) {
 	if c.Username == "" || c.Password == "" {
 		return "", &InvalidCredentials{}
 	}
-	if c.Username == os.Getenv("USERNAME") {
-		if c.Password == os.Getenv("PASSWORD") {
+	if c.Username == authSrc.AdminUsername() {
+		if c.Password == authSrc.AdminPassword() {
 			id := uuid.New()
 			cache.update(id.String(), id.String())
 			return id.String(), nil

+ 83 - 0
pkg/auth/auth_test.go

@@ -0,0 +1,83 @@
+package auth
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// Implementing the Source interface
+type testAuthSource struct {
+	user string
+	pass string
+}
+
+func (tst testAuthSource) AdminUsername() string { return tst.user }
+func (tst testAuthSource) AdminPassword() string { return tst.pass }
+
+/*
+Table testing the authorize function
+*/
+func TestAuthorize(t *testing.T) {
+	type authTestCase struct {
+		desc          string
+		inputUsername string
+		inputPassword string
+		realUsername  string
+		realPassword  string
+		expectError   error
+	}
+	cache := NewCache()
+	for _, tc := range []authTestCase{
+		{
+			desc:          "Passing test case where auth works",
+			inputUsername: "admin",
+			inputPassword: "abc123",
+			realUsername:  "admin",
+			realPassword:  "abc123",
+			expectError:   nil,
+		},
+		{
+			desc:          "Auth fails because username is empty",
+			inputUsername: "",
+			inputPassword: "abc123",
+			realUsername:  "admin",
+			realPassword:  "abc123",
+			expectError:   &InvalidCredentials{},
+		},
+		{
+			desc:          "Auth fails because password is empty",
+			inputUsername: "admin",
+			inputPassword: "",
+			realUsername:  "admin",
+			realPassword:  "abc123",
+			expectError:   &InvalidCredentials{},
+		},
+		{
+			desc:          "Auth fails because password is wrong",
+			inputUsername: "admin",
+			inputPassword: "xyz987",
+			realUsername:  "admin",
+			realPassword:  "abc123",
+			expectError:   &InvalidCredentials{},
+		},
+		{
+			desc:          "Auth fails because username is wrong",
+			inputUsername: "admin",
+			inputPassword: "",
+			realUsername:  "admin",
+			realPassword:  "abc123",
+			expectError:   &InvalidCredentials{},
+		},
+	} {
+		t.Run(tc.desc, func(t *testing.T) {
+
+			_, err := Authorize(&Credentials{Username: tc.inputUsername,
+				Password: tc.inputPassword},
+				cache,
+				testAuthSource{user: tc.realUsername, pass: tc.realPassword})
+			assert.Equal(t, tc.expectError, err)
+
+		})
+	}
+}

+ 1 - 1
pkg/controller/admin_handlers.go

@@ -41,7 +41,7 @@ func (c *Controller) Auth(ctx *gin.Context) {
 		})
 		return
 	}
-	cookie, err := auth.Authorize(&cred, c.Cache)
+	cookie, err := auth.Authorize(&cred, c.Cache, c.AuthSource)
 	if err != nil {
 		ctx.JSON(400, map[string]string{
 			"Error": err.Error(),

+ 11 - 9
pkg/controller/controller.go

@@ -8,17 +8,19 @@ import (
 )
 
 type Controller struct {
-	Domain   string
-	database storage.DocumentIO
-	Cache    *auth.AuthCache
-	FileIO   fs.FS
+	Domain     string
+	database   storage.DocumentIO
+	Cache      *auth.AuthCache
+	AuthSource auth.Source
+	FileIO     fs.FS
 }
 
-func NewController(domain string, database storage.DocumentIO, files fs.FS) *Controller {
+func NewController(domain string, database storage.DocumentIO, files fs.FS, authSrc auth.Source) *Controller {
 	return &Controller{
-		Cache:    auth.NewCache(),
-		Domain:   domain,
-		database: database,
-		FileIO:   files,
+		Cache:      auth.NewCache(),
+		AuthSource: authSrc,
+		Domain:     domain,
+		database:   database,
+		FileIO:     files,
 	}
 }

+ 38 - 12
pkg/env/env.go

@@ -1,13 +1,17 @@
 package env
 
 import (
+	"bytes"
 	"fmt"
-	"log"
+	"io"
 	"os"
+	"sort"
 
 	"github.com/joho/godotenv"
 )
 
+const KEIJI_USERNAME = "KEIJI_USERNAME"
+const KEIJI_PASSWORD = "KEIJI_PASSWORD"
 const IMAGE_STORE = "IMAGE_STORE"
 const WEB_ROOT = "WEB_ROOT"
 const DOMAIN_NAME = "DOMAIN_NAME"
@@ -25,10 +29,12 @@ var OPTION_VARS = map[string]string{
 }
 
 var REQUIRED_VARS = map[string]string{
-	HOST_PORT:   "#the port to run the server on (int)",
-	HOST_ADDR:   "#the address for the server to listen on (string)",
-	DOMAIN_NAME: "#the servers domain name, i.e. 'aetherial.dev', or 'localhost' (string)",
-	SSL_MODE:    "#chose to use SSL or not (boolean)",
+	HOST_PORT:      "#the port to run the server on (int)",
+	HOST_ADDR:      "#the address for the server to listen on (string)",
+	DOMAIN_NAME:    "#the servers domain name, i.e. 'aetherial.dev', or 'localhost' (string)",
+	SSL_MODE:       "#chose to use SSL or not (boolean)",
+	KEIJI_USERNAME: "#the administrator username to login with",
+	KEIJI_PASSWORD: "#the password for the administrator accounit",
 }
 
 type EnvNotSet struct {
@@ -45,21 +51,41 @@ optional configuration (commented out)
 
 	:param path: the path to write the template to
 */
-func WriteTemplate(path string) {
+func WriteTemplate(wtr io.Writer) error {
+
+	outReqArr := make([]string, len(REQUIRED_VARS))
+	outOptVar := make([]string, len(OPTION_VARS))
+	i := 0
+	for k := range REQUIRED_VARS {
+		outReqArr[i] = k
+		i++
+	}
+	i = 0
+	for k := range OPTION_VARS {
+		outOptVar[i] = k
+		i++
+	}
+	sort.Strings(outReqArr)
+	sort.Strings(outOptVar)
+
 	var out string
 	out = out + "####### Required Configuration #######\n"
-	for k, v := range REQUIRED_VARS {
+	for i := range outReqArr {
+		k := REQUIRED_VARS[outReqArr[i]]
+		v := outReqArr[i]
+		fmt.Println(k, v)
 		out = out + fmt.Sprintf("%s\n%s=\n", v, k)
 	}
 	out = out + "\n####### Optional Configuration #######\n"
-	for k, v := range OPTION_VARS {
-		out = out + fmt.Sprintf("# %s\n# %s=\n", v, k)
+	for i := range outOptVar {
+		out = out + fmt.Sprintf("# %s\n# %s=\n", OPTION_VARS[outOptVar[i]], outOptVar[i])
 	}
-	err := os.WriteFile(path, []byte(out), os.ModePerm)
+	msg := []byte(out)
+	_, err := io.Copy(wtr, bytes.NewBuffer(msg))
 	if err != nil {
-		log.Fatal("Failed to write file: ", err)
+		return err
 	}
-
+	return nil
 }
 
 /*

+ 27 - 0
pkg/env/env_test.go

@@ -0,0 +1,27 @@
+package env
+
+import (
+	"bytes"
+	"io"
+	"log"
+	"os"
+	"testing"
+)
+
+// Testing that the template writer creates the appropriate data
+func TestWriteTemplate(t *testing.T) {
+	b, err := os.ReadFile("../../test/.env.template")
+	if err != nil {
+		log.Fatal(err)
+	}
+	buf := bytes.NewBuffer([]byte{})
+	err = WriteTemplate(buf)
+	if err != nil {
+		log.Fatal(err)
+	}
+	got, err := io.ReadAll(buf)
+	if string(got) != string(b) {
+		t.Errorf("test failed! Got: %s\nWanted: %s\n", string(got), string(b))
+	}
+
+}

+ 3 - 2
pkg/routes/register.go

@@ -3,13 +3,14 @@ package routes
 import (
 	"io/fs"
 
+	"git.aetherial.dev/aeth/keiji/pkg/auth"
 	"git.aetherial.dev/aeth/keiji/pkg/controller"
 	"git.aetherial.dev/aeth/keiji/pkg/storage"
 	"github.com/gin-gonic/gin"
 )
 
-func Register(e *gin.Engine, domain string, database storage.DocumentIO, files fs.FS) {
-	c := controller.NewController(domain, database, files)
+func Register(e *gin.Engine, domain string, database storage.DocumentIO, files fs.FS, authSrc auth.Source) {
+	c := controller.NewController(domain, database, files, authSrc)
 	web := e.Group("")
 	web.GET("/", c.ServeHome)
 	web.GET("/blog", c.ServeBlog)

+ 1 - 85
pkg/storage/storage.go

@@ -207,67 +207,6 @@ func (r *SQLiteRepo) Migrate() error {
 	return nil
 }
 
-/*
-Seed the database with the necessary configuration items to function properly
-
-	:param menu: the text file containing the k/v pair for the navigation menu
-	:param pngs: the text file containing the k/v pair for the icon names -> redirect links
-	:param dir: the directory that the PNG assets are in (note: the k/v pair in pngs will read from this dir)
-*/
-func (s *SQLiteRepo) Seed(menu string, pngs string, dir string) { // TODO: make a bootstrap file with a comprehensive unmarshalling sequence for tighter control of the seeing procedute
-	b, err := os.ReadFile(menu)
-
-	if err != nil {
-		log.Fatal(err)
-	}
-	entries := strings.Split(string(b), "\n")
-	for i := range entries {
-		if entries[i] == "" {
-			continue
-		}
-		info := strings.Split(entries[i], "=")
-		err := s.AddMenuItem(LinkPair{Link: info[0], Text: info[1]})
-		if err != nil {
-			log.Fatal(err)
-		}
-	}
-	b, err = os.ReadFile(pngs)
-	if err != nil {
-		log.Fatal(err)
-	}
-	entries = strings.Split(string(b), "\n")
-	for i := range entries {
-		if entries[i] == "" {
-			continue
-		}
-		info := strings.Split(entries[i], "=")
-		b, err := os.ReadFile(path.Join(dir, info[0]))
-		if err != nil {
-			log.Fatal(err)
-		}
-		err = s.AddNavbarItem(NavBarItem{Png: b, Link: info[0], Redirect: info[1]})
-		if err != nil {
-			log.Fatal(err)
-		}
-	}
-	assets, err := os.ReadDir(dir)
-	if err != nil {
-		log.Fatal(err)
-	}
-	for i := range assets {
-		b, err := os.ReadFile(path.Join(dir, assets[i].Name()))
-		if err != nil {
-			log.Fatal(err)
-		}
-		err = s.AddAsset(assets[i].Name(), b)
-		if err != nil {
-			log.Fatal(err)
-		}
-
-	}
-
-}
-
 /*
 Get all dropdown menu elements. Returns a list of LinkPair structs with the text and redirect location
 */
@@ -406,10 +345,7 @@ 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 string
-	var location string
-	var desc string
-	var created string
+	var title, location, desc, created string
 	if err := row.Scan(&rowNum, &title, &location, &desc, &created); err != nil {
 		if errors.Is(err, sql.ErrNoRows) {
 			return Image{}, ErrNotExists
@@ -668,26 +604,6 @@ type ImageStoreItem struct {
 	ApiPath      string
 }
 
-/*
-Create a new ImageStoreItem
-
-	:param fname: the name of the file to be saved
-	:param title: the canonical title to give the image
-	:param desc: the description to associate to the image
-*/
-func NewImageStoreItem(title string, desc string) Image {
-	id := newIdentifier()
-	img := Image{
-		Ident:    id,
-		Title:    title,
-		Category: DIGITAL_ART,
-		Location: GetImageStore(),
-		Created:  time.Now().UTC().String(),
-		Desc:     desc,
-	}
-	return img
-}
-
 /*
 Function to return the location of the image store. Wrapping the env call in
 a function so that refactoring is easier

+ 302 - 0
pkg/storage/storage_test.go

@@ -0,0 +1,302 @@
+package storage
+
+import (
+	"database/sql"
+	"log"
+	"testing"
+
+	_ "github.com/mattn/go-sqlite3"
+	"github.com/stretchr/testify/assert"
+)
+
+func newTestDb() (*SQLiteRepo, *sql.DB) {
+
+	db, err := sql.Open("sqlite3", ":memory:")
+	if err != nil {
+		log.Fatal(err)
+	}
+	testDb := &SQLiteRepo{db: db}
+	err = testDb.Migrate()
+	if err != nil {
+		log.Fatal("failed to start the test database: ", err)
+	}
+	return testDb, db
+}
+
+func TestMigrate(t *testing.T) {
+	requiredTables := []string{
+		"posts",
+		"images",
+		"menu",
+		"navbar",
+		"assets",
+		"admin",
+	}
+
+	db, err := sql.Open("sqlite3", ":memory:")
+	if err != nil {
+		log.Fatal(err)
+	}
+	testDb := &SQLiteRepo{db: db}
+	err = testDb.Migrate()
+	if err != nil {
+		t.Error(err)
+	}
+	for i := range requiredTables {
+		name := requiredTables[i]
+		row := db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='?'", name)
+		if row.Err() != nil {
+			t.Errorf("error querying table: %s", name)
+		}
+		if row == nil {
+			t.Errorf("no table returned: %s", name)
+		}
+	}
+
+}
+
+func TestGetDropdownElements(t *testing.T) {
+	type testcase struct {
+		seed []LinkPair
+	}
+	testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			seed: []LinkPair{
+				{
+					Text: "abc123",
+					Link: "/abc/123",
+				},
+			},
+		},
+	} {
+		stmt, _ := db.Prepare("INSERT INTO menu(link, text) VALUES (?,?)")
+		for i := range tc.seed {
+			_, err := stmt.Exec(tc.seed[i].Link, tc.seed[i].Text)
+			if err != nil {
+				t.Errorf("failed to seed: %s", err)
+			}
+		}
+		got := testDb.GetDropdownElements()
+		assert.Equal(t, got, tc.seed)
+
+	}
+
+}
+func TestGetNavBarLinks(t *testing.T) {
+	type testcase struct {
+		seed []NavBarItem
+	}
+	testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			seed: []NavBarItem{
+				{
+					Link:     "/abc/123",
+					Redirect: "/abc/123/site",
+					Png:      []byte("xzy123abc098"),
+				},
+			},
+		},
+	} {
+		stmt, _ := db.Prepare("INSERT INTO navbar(png, link, redirect) VALUES (?,?,?)")
+		for i := range tc.seed {
+			_, err := stmt.Exec(tc.seed[i].Png, tc.seed[i].Link, tc.seed[i].Redirect)
+			if err != nil {
+				t.Errorf("failed to seed: %s", err)
+			}
+		}
+		got := testDb.GetNavBarLinks()
+		assert.Equal(t, tc.seed, got)
+
+	}
+}
+func TestGetAssets(t *testing.T) {
+	type testcase struct {
+		seed []Asset
+	}
+	testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			seed: []Asset{
+				{
+					Data: []byte("abc123xyz098"),
+					Name: "asset1",
+				},
+			},
+		},
+	} {
+		stmt, _ := db.Prepare("INSERT INTO assets(data, name) VALUES (?,?)")
+		for i := range tc.seed {
+			_, err := stmt.Exec(tc.seed[i].Data, tc.seed[i].Name)
+			if err != nil {
+				t.Error(err)
+			}
+		}
+		got := testDb.GetAssets()
+		assert.Equal(t, tc.seed, got)
+
+	}
+}
+
+func TestGetAdminTables(t *testing.T) {
+
+	type testcase struct {
+		seed AdminPage
+	}
+	testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			seed: AdminPage{
+				Tables: map[string][]TableData{
+					"test": {
+						{
+							DisplayName: "abc123",
+							Link:        "xyz098",
+						},
+					},
+				},
+			},
+		},
+	} {
+		stmt, _ := db.Prepare("INSERT INTO admin(display_name, link, category) VALUES (?,?,?)")
+		for k, table := range tc.seed.Tables {
+			for i := range table {
+				_, err := stmt.Exec(table[i].DisplayName, table[i].Link, k)
+				if err != nil {
+					t.Error(err)
+				}
+
+			}
+		}
+		got := testDb.GetAdminTables()
+		assert.Equal(t, tc.seed, got)
+
+	}
+}
+func TestGetDocument(t *testing.T) {
+
+	type testcase struct {
+		seed Document
+	}
+	testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			seed: Document{
+				Ident:    Identifier("qwerty"),
+				Title:    "abc 123",
+				Created:  "2024-12-31",
+				Body:     "blog post body etc",
+				Category: BLOG,
+				Sample:   "this is a sample",
+			},
+		},
+	} {
+		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)
+		}
+		got, _ := testDb.GetDocument(Identifier("qwerty"))
+		assert.Equal(t, tc.seed, got)
+
+	}
+}
+func TestGetByCategory(t *testing.T) {
+
+	type testcase struct {
+		seed []Document
+	}
+	testDb, db := newTestDb()
+	for _, tc := range []testcase{
+		{
+			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",
+				},
+				{
+					Row:      2,
+					Ident:    Identifier("poiuyt"),
+					Title:    "abc 123",
+					Created:  "2024-12-31",
+					Body:     "blog post body etc",
+					Category: BLOG,
+					Sample:   "this is a sample",
+				},
+			},
+		},
+	} {
+		stmt, _ := db.Prepare("INSERT INTO posts(id, title, created, body, category, sample) VALUES (?,?,?,?,?,?)")
+		for i := range tc.seed {
+			_, err := stmt.Exec(tc.seed[i].Ident, tc.seed[i].Title, tc.seed[i].Created, tc.seed[i].Body, tc.seed[i].Category, tc.seed[i].Sample)
+			if err != nil {
+				t.Error(err)
+			}
+		}
+		got := testDb.GetByCategory(BLOG)
+		assert.Equal(t, tc.seed, got)
+	}
+
+}
+func TestGetImage(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestGetAllImages(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+
+func TestAllDocuments(t *testing.T) {
+	// testDb, db := newTestDb()
+}
+
+func TestUpdateDocument(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+
+func TestAddImage(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestAddMenuItem(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestAddNavbarItem(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestAddAsset(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestAddDocument(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestAddAdminTableEntry(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+
+func TestDeleteDocument(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+
+func TestGetImageStore(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}
+func TestNewIdentifier(t *testing.T) {
+
+	// testDb, db := newTestDb()
+}

+ 1 - 0
pkg/webpages/pages_test.go

@@ -0,0 +1 @@
+package webpages