From 5e8b4e2b61d78fd1b40c55c1c4265e36a7fe3c7f Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 4 Nov 2025 16:39:10 -0500 Subject: [PATCH 1/3] Adding initial code for adding a new poll on the backend --- go.mod | 2 +- server/config/config.go | 14 ++++++--- server/db/db.go | 49 +++++++++++++++++++++++--------- server/db/db_format.sql | 24 ---------------- server/db/db_test.go | 21 ++++++++------ server/models/poll.go | 2 +- server/services/poll.go | 41 ++++++++++++++++++++++++++ server/services/services_test.go | 45 +++++++++++++++++++++++++++++ 8 files changed, 147 insertions(+), 51 deletions(-) delete mode 100644 server/db/db_format.sql create mode 100644 server/services/poll.go create mode 100644 server/services/services_test.go diff --git a/go.mod b/go.mod index c5eda1c..6ec68ad 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go-sjles-pta-vote/server +module go-sjles-pta-vote go 1.24.4 diff --git a/server/config/config.go b/server/config/config.go index 4621721..0c49a67 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -14,6 +14,7 @@ type Config struct { } var conf *Config +var conf_path string = ".env" func GetConfig() *Config { if conf != nil { @@ -23,7 +24,7 @@ func GetConfig() *Config { conf = &Config{} // TODO: Make this into a ini or toml file - confgContent, err := os.ReadFile(".env") + configContent, err := os.ReadFile(conf_path) if err != nil { fmt.Println("Error reading .env file: ", err) @@ -37,10 +38,15 @@ func GetConfig() *Config { for _, variable := range envVariables { if strings.Contains(variable, "=") { splitVariable := strings.Split(variable, "=") - envMap[splitVariable[0]] = splitVAriable[1] + envMap[splitVariable[0]] = splitVariable[1] } } + if err := json.Unmarshal([]byte(configContent), conf); err != nil { + fmt.Println("Error unmarshalling config file: ", err) + os.Exit(1) + } + // TODO: Better mapping of key to json values // TODO: Better error checking if values are missing // TODO: Default values @@ -59,6 +65,6 @@ func GetConfig() *Config { return conf } -func init() { - conf = GetConfig() +func SetConfig(init_conf *Config) { + conf = init_conf } \ No newline at end of file diff --git a/server/db/db.go b/server/db/db.go index 60c1ccc..aa4c1f3 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -2,30 +2,53 @@ package db import ( "database/sql" - "os" + + "go-sjles-pta-vote/server/config" _ "github.com/glebarez/go-sqlite" ) +var build_db_query string = ` +CREATE TABLE IF NOT EXISTS polls ( + id INTEGER PRIMARY KEY, + question TEXT NOT NULL, + member_yes_votes UNSIGNED INT NOT NULL DEFAULT 0, + member_no_votes UNSIGNED INT NOT NULL DEFAULT 0, + non_member_yes_votes UNSIGNED INT NOT NULL DEFAULT 0, + non_member_no_votes UNSIGNED INT NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + expires_at DATETIME +); + +CREATE TABLE IF NOT EXISTS voters ( + poll_id UNSIGNED INT NOT NULL, + voter_email TEXT NOT NULL, + FOREIGN KEY (poll_id) REFERENCES polls(id), + PRIMARY KEY (poll_id, voter_email) +); + +CREATE TABLE IF NOT EXISTS members ( + email TEXT NOT NULL, + member_name TEXT, + PRIMARY KEY (email) +); +` + var db *sql.DB -func Connect(db_path string) error { - var err error - db, err = sql.Open("sqlite", db_path) +func Connect() (*sql.DB, error) { + db_config := config.GetConfig() + + db, err := sql.Open("sqlite", db_config.DBPath) if err != nil { - return err + return nil, err } - sql_create, err := os.ReadFile("./db_format.sql") + _, err = db.Exec(build_db_query) - if err != nil { - return err - } - - _, err = db.Exec(string(sql_create)) - - return err + return db, err } func Close() { diff --git a/server/db/db_format.sql b/server/db/db_format.sql deleted file mode 100644 index 1721e07..0000000 --- a/server/db/db_format.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE IF NOT EXISTS polls ( - id INTEGER PRIMARY KEY, - question TEXT NOT NULL, - member_yes_votes UNSIGNED INT NOT NULL, - member_no_votes UNSIGNED INT NOT NULL, - non_member_yes_votes UNSIGNED INT NOT NULL, - non_member_no_votes UNSIGNED INT NOT NULL, - created_at DATETIME, - updated_at DATETIME, - expires_at DATETIME -); - -CREATE TABLE IF NOT EXISTS voters ( - poll_id UNSIGNED INT NOT NULL, - voter_email TEXT NOT NULL, - FOREIGN KEY (poll_id) REFERENCES polls(id), - PRIMARY KEY (poll_id, voter_email) -); - -CREATE TABLE IF NOT EXISTS members ( - email TEXT NOT NULL, - member_name TEXT, - PRIMARY KEY (email) -); diff --git a/server/db/db_test.go b/server/db/db_test.go index ef57785..e4e32b0 100644 --- a/server/db/db_test.go +++ b/server/db/db_test.go @@ -3,21 +3,26 @@ package db import ( "os" "testing" + + "go-sjles-pta-vote/server/config" ) func TestConnect(t *testing.T) { tmp_db, err := os.CreateTemp("", "vote_test.*.db") - tmp_db_name := tmp_db.Name() - tmp_db.Close() - - defer os.Remove(tmp_db_name) - if err != nil { - t.Errorf(`Failed to create temporary db: %v`, err) + t.Errorf(`Failed to create temporary db file: %v`, err) } - if err := Connect(tmp_db_name); err != nil { - t.Errorf(`Failed to create the database at %s: %v`, tmp_db_name, err) + init_conf := &config.Config{ + DBPath: string(tmp_db.Name()), + } + config.SetConfig(init_conf) + + defer os.Remove(tmp_db.Name()) + tmp_db.Close() + + if _, err := Connect(); err != nil { + t.Errorf(`Failed to create the database: %v`, err) } defer Close() diff --git a/server/models/poll.go b/server/models/poll.go index 078c62e..fcc1505 100644 --- a/server/models/poll.go +++ b/server/models/poll.go @@ -1,7 +1,7 @@ package models type Poll struct { - ID int `json:"id"` + ID int64 `json:"id"` Question string `json:"question"` TotalVotes int `json:"total_votes"` WhoVoted []string `json:"who_voted"` diff --git a/server/services/poll.go b/server/services/poll.go new file mode 100644 index 0000000..56a4d8e --- /dev/null +++ b/server/services/poll.go @@ -0,0 +1,41 @@ +package services + +import ( + "go-sjles-pta-vote/server/db" + "go-sjles-pta-vote/server/models" +) + +func CreatePoll(poll *models.Poll) (*models.Poll, error) { + new_poll := models.Poll{} + + db_conn, err := db.Connect() + if err != nil { + return nil, err + } + defer db.Close() + + stmt, err := db_conn.Prepare(` + INSERT INTO polls ( + question, + expires_at + ) VALUES ( + $1, + $2 + ) RETURNING ID; + `) + + if err != nil { + return nil, err + } + + defer stmt.Close() + + res, err := stmt.Exec(poll.Question, poll.ExpiresAt) + if err != nil { + return nil, err + } + + new_poll.ID, err = res.LastInsertId() + + return &new_poll, err +} \ No newline at end of file diff --git a/server/services/services_test.go b/server/services/services_test.go new file mode 100644 index 0000000..a5d0a5d --- /dev/null +++ b/server/services/services_test.go @@ -0,0 +1,45 @@ +package services + +import ( + "os" + "testing" + "time" + + "go-sjles-pta-vote/server/config" + "go-sjles-pta-vote/server/db" + "go-sjles-pta-vote/server/models" +) + +func TestCreatePoll(t *testing.T) { + tmp_db, err := os.CreateTemp("", "vote_test.*.db") + if err != nil { + t.Errorf(`Failed to create temporary db file: %v`, err) + } + + init_conf := &config.Config{ + DBPath: string(tmp_db.Name()), + } + config.SetConfig(init_conf) + + defer os.Remove(tmp_db.Name()) + tmp_db.Close() + + if _, err := db.Connect(); err != nil { + t.Errorf(`Failed to create the database: %v`, err) + } + + create_poll := &models.Poll{ + Question: "TestQuestion", + ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), + } + + new_poll, err := CreatePoll(create_poll) + + if err != nil { + t.Errorf(`Failed to insert into table: %v`, err) + } + + if new_poll.ID != 1 { + t.Errorf(`Failed to insert into table: Index %d: error %v`, new_poll.ID, err) + } +} \ No newline at end of file -- 2.49.1 From 5d579781a8ad7aeba8e5f3031cbeb177d8eebf6f Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 4 Nov 2025 17:16:42 -0500 Subject: [PATCH 2/3] More robust testing --- server/services/poll.go | 22 ++++++++++++++++++++++ server/services/services_test.go | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/server/services/poll.go b/server/services/poll.go index 56a4d8e..0e2513a 100644 --- a/server/services/poll.go +++ b/server/services/poll.go @@ -1,6 +1,9 @@ package services import ( + "database/sql" + "errors" + "go-sjles-pta-vote/server/db" "go-sjles-pta-vote/server/models" ) @@ -13,6 +16,25 @@ func CreatePoll(poll *models.Poll) (*models.Poll, error) { return nil, err } defer db.Close() + + get_stmt, err := db_conn.Prepare(` + SELECT id + FROM polls + WHERE question == $1 + `) + if err != nil { + return nil, err + } + defer get_stmt.Close() + + var id int + err = get_stmt.QueryRow(poll.Question).Scan(&id) + if err != sql.ErrNoRows { + if err != nil { + return nil, err + } + return nil, errors.New("Already exists") + } stmt, err := db_conn.Prepare(` INSERT INTO polls ( diff --git a/server/services/services_test.go b/server/services/services_test.go index a5d0a5d..a137abc 100644 --- a/server/services/services_test.go +++ b/server/services/services_test.go @@ -39,7 +39,7 @@ func TestCreatePoll(t *testing.T) { t.Errorf(`Failed to insert into table: %v`, err) } - if new_poll.ID != 1 { - t.Errorf(`Failed to insert into table: Index %d: error %v`, new_poll.ID, err) + if new_poll == nil { + t.Errorf(`Failed to insert into table`) } } \ No newline at end of file -- 2.49.1 From 7ad8a1bcc67bf3f65dafb8edd7f8bab7f02fc772 Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 5 Nov 2025 15:47:26 -0500 Subject: [PATCH 3/3] More robust testing --- server/services/poll.go | 4 +- server/services/services_test.go | 79 ++++++++++++++++++++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/server/services/poll.go b/server/services/poll.go index 0e2513a..ac22f3f 100644 --- a/server/services/poll.go +++ b/server/services/poll.go @@ -8,6 +8,8 @@ import ( "go-sjles-pta-vote/server/models" ) +var ErrQuestionAlreadyExists = errors.New("Question already exists") + func CreatePoll(poll *models.Poll) (*models.Poll, error) { new_poll := models.Poll{} @@ -33,7 +35,7 @@ func CreatePoll(poll *models.Poll) (*models.Poll, error) { if err != nil { return nil, err } - return nil, errors.New("Already exists") + return nil, ErrQuestionAlreadyExists } stmt, err := db_conn.Prepare(` diff --git a/server/services/services_test.go b/server/services/services_test.go index a137abc..1e71387 100644 --- a/server/services/services_test.go +++ b/server/services/services_test.go @@ -1,6 +1,7 @@ package services import ( + "math/rand" "os" "testing" "time" @@ -10,7 +11,71 @@ import ( "go-sjles-pta-vote/server/models" ) +const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=`~!@#$%^&*()_+[]\\;',./{}|:\"<>?" + +func RandString(length int) string { + rand_bytes := make([]byte, length) + for rand_index := range length { + rand_bytes[rand_index] = charset[rand.Intn(len(charset))] + } + return string(rand_bytes) +} + func TestCreatePoll(t *testing.T) { + parameters := []struct{ + question string + table_index int64 + }{ + {RandString(10) + "1", 1}, + {RandString(10) + "2", 2}, + {RandString(10) + "3", 3}, + {"\"" + RandString(10) + "4", 4}, + {"'" + RandString(10) + "5", 5}, + {";" + RandString(10) + "6", 6}, + } + + tmp_db, err := os.CreateTemp("", "vote_test.*.db") + if err != nil { + t.Errorf(`Failed to create temporary db file: %v`, err) + } + + init_conf := &config.Config{ + DBPath: string(tmp_db.Name()), + } + config.SetConfig(init_conf) + + defer os.Remove(tmp_db.Name()) + tmp_db.Close() + + if _, err := db.Connect(); err != nil { + t.Errorf(`Failed to create the database: %v`, err) + } + + for i := range parameters { + create_poll := &models.Poll{ + Question: parameters[i].question, + ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), + } + + new_poll, err := CreatePoll(create_poll) + + if err != nil { + t.Fatalf(`Failed to create new poll %s: %v`, parameters[i].question, err) + } + + if new_poll == nil { + t.Fatalf(`Failed to insert %s into table`, parameters[i].question) + } + + if new_poll.ID != parameters[i].table_index { + t.Fatalf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll.ID) + } + } +} + +func TestAlreadyExists(t *testing.T) { + question := "TestQuestion" + tmp_db, err := os.CreateTemp("", "vote_test.*.db") if err != nil { t.Errorf(`Failed to create temporary db file: %v`, err) @@ -29,17 +94,23 @@ func TestCreatePoll(t *testing.T) { } create_poll := &models.Poll{ - Question: "TestQuestion", + Question: question, ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"), } new_poll, err := CreatePoll(create_poll) - + if err != nil { - t.Errorf(`Failed to insert into table: %v`, err) + t.Fatalf(`Failed to create new poll %s: %v`, question, err) } if new_poll == nil { - t.Errorf(`Failed to insert into table`) + t.Fatalf(`Failed to insert %s into table`, question) + } + + new_poll, err = CreatePoll(create_poll) + + if err != ErrQuestionAlreadyExists { + t.Fatalf(`Should have failed adding %s as it already exists`, question) } } \ No newline at end of file -- 2.49.1