Merge pull request 'Newpoll Backend' (#12) from create-new-poll-backend into main

Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2025-11-05 20:48:15 +00:00
8 changed files with 242 additions and 51 deletions

2
go.mod
View File

@@ -1,4 +1,4 @@
module go-sjles-pta-vote/server
module go-sjles-pta-vote
go 1.24.4

View File

@@ -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
}

View File

@@ -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() {

View File

@@ -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)
);

View File

@@ -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()

View File

@@ -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"`

65
server/services/poll.go Normal file
View File

@@ -0,0 +1,65 @@
package services
import (
"database/sql"
"errors"
"go-sjles-pta-vote/server/db"
"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{}
db_conn, err := db.Connect()
if err != nil {
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, ErrQuestionAlreadyExists
}
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
}

View File

@@ -0,0 +1,116 @@
package services
import (
"math/rand"
"os"
"testing"
"time"
"go-sjles-pta-vote/server/config"
"go-sjles-pta-vote/server/db"
"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)
}
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: 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`, question, err)
}
if new_poll == nil {
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)
}
}