Adding basic backend to handle RSVPs
This commit is contained in:
parent
b2ad1654d1
commit
3e04608d88
10 changed files with 624 additions and 0 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"cSpell.words": ["authd", "gotfy", "ntfy", "Rsvps"]
|
||||||
|
}
|
||||||
46
server/controller.go
Normal file
46
server/controller.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
db *sql.DB
|
||||||
|
ntfy *Ntfy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Printf("%s - [%s] (%s) %s\n", time.Now().Format(time.RFC3339), r.RemoteAddr, r.Method, r.URL)
|
||||||
|
|
||||||
|
switch true {
|
||||||
|
case r.Method == "GET" && r.URL.Path == "/":
|
||||||
|
rsvps, err := GetRsvps(h.db)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, "%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintf(w, "%#v", rsvps)
|
||||||
|
case r.Method == "POST" && r.URL.Path == "/":
|
||||||
|
var rsvp Rsvp
|
||||||
|
|
||||||
|
json.NewDecoder(r.Body).Decode(&r)
|
||||||
|
_, err := rsvp.CreateRsvp(h.db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SendRsvpNotification(h.ntfy, &rsvp)
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
server/database.go
Normal file
26
server/database.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupDatabase() *sql.DB {
|
||||||
|
db, err := sql.Open("sqlite3", "./db.sqlite")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to open database")
|
||||||
|
}
|
||||||
|
|
||||||
|
var version string
|
||||||
|
err = db.QueryRow("SELECT SQLITE_VERSION()").Scan(&version)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("failed to query database")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(version)
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
10
server/go.mod
Normal file
10
server/go.mod
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
module collinenlucy.nl
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/AnthonyHewins/gotfy v0.0.10 // indirect
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||||
|
)
|
||||||
9
server/go.sum
Normal file
9
server/go.sum
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
github.com/AnthonyHewins/gotfy v0.0.10 h1:23ZjRVG7wuGuqn7CQq/bOrkXa3gg2XyYrrD3RYEmvE8=
|
||||||
|
github.com/AnthonyHewins/gotfy v0.0.10/go.mod h1:q2orErDDpl9/gZ5L4oJhejb7TaP/eBdtkzjWDruNRlg=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
17
server/main.go
Normal file
17
server/main.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ntfy := SetupNtfyClient()
|
||||||
|
|
||||||
|
db := SetupDatabase()
|
||||||
|
SetupRsvpsTable(db)
|
||||||
|
|
||||||
|
hnd := &Handler{db, ntfy}
|
||||||
|
http.ListenAndServe(":8000", hnd)
|
||||||
|
}
|
||||||
152
server/ntfy.go
Normal file
152
server/ntfy.go
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AnthonyHewins/gotfy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authdTransport struct {
|
||||||
|
underlyingTransport http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *authdTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
var ntfyToken string
|
||||||
|
env := os.Environ()
|
||||||
|
for _, keyVal := range env {
|
||||||
|
keyValArr := strings.Split(keyVal, "=")
|
||||||
|
if keyValArr[0] == "NTFY_TOKEN" {
|
||||||
|
ntfyToken = keyValArr[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", "Bearer "+ntfyToken)
|
||||||
|
return t.underlyingTransport.RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ntfy struct {
|
||||||
|
client *gotfy.Publisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupNtfyClient() *Ntfy {
|
||||||
|
server, err := url.Parse("https://home.collinduncan.com/ntfy")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authdHttpClient := &http.Client{Transport: &authdTransport{underlyingTransport: http.DefaultTransport}}
|
||||||
|
|
||||||
|
tp, err := gotfy.NewPublisher(server, authdHttpClient)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Ntfy{client: tp}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTitle(rsvp *Rsvp) string {
|
||||||
|
builder := new(strings.Builder)
|
||||||
|
if rsvp.Attending {
|
||||||
|
peoplePerson := "people"
|
||||||
|
if rsvp.PartySize == 1 {
|
||||||
|
peoplePerson = "person"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(builder, "%d %s confirmed!", rsvp.PartySize, peoplePerson)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(builder, "Someone can't make it")
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMessage(rsvp *Rsvp) string {
|
||||||
|
builder := new(strings.Builder)
|
||||||
|
if rsvp.Attending {
|
||||||
|
builder.WriteString("Here's who is coming 👇")
|
||||||
|
for _, mem := range rsvp.PartyMembers {
|
||||||
|
age := "adult"
|
||||||
|
if mem.Child {
|
||||||
|
age = "👶"
|
||||||
|
} else {
|
||||||
|
age = "🧓"
|
||||||
|
}
|
||||||
|
dp := "n/a"
|
||||||
|
if len(mem.DietaryPreferences) > 0 {
|
||||||
|
dp = mem.DietaryPreferences
|
||||||
|
}
|
||||||
|
fmt.Fprintf(builder, "\n- %s %s, %s", age, mem.Name, dp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, mem := range rsvp.PartyMembers {
|
||||||
|
fmt.Fprintf(builder, "%s\n", mem.Name)
|
||||||
|
}
|
||||||
|
builder.WriteString("can't make it")
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberToEmoji(num int64) string {
|
||||||
|
switch num {
|
||||||
|
case 0:
|
||||||
|
return "zero"
|
||||||
|
case 1:
|
||||||
|
return "one"
|
||||||
|
case 2:
|
||||||
|
return "two"
|
||||||
|
case 3:
|
||||||
|
return "three"
|
||||||
|
case 4:
|
||||||
|
return "four"
|
||||||
|
case 5:
|
||||||
|
return "five"
|
||||||
|
case 6:
|
||||||
|
return "six"
|
||||||
|
case 7:
|
||||||
|
return "seven"
|
||||||
|
case 8:
|
||||||
|
return "eight"
|
||||||
|
case 9:
|
||||||
|
return "nine"
|
||||||
|
default:
|
||||||
|
return "question"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTags(rsvp *Rsvp) []string {
|
||||||
|
if rsvp.Attending {
|
||||||
|
return []string{"white_check_mark", numberToEmoji(rsvp.PartySize)}
|
||||||
|
} else {
|
||||||
|
return []string{"x"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildNtfyMessage(topic string, rsvp *Rsvp) *gotfy.Message {
|
||||||
|
return &gotfy.Message{
|
||||||
|
Topic: topic,
|
||||||
|
Message: buildMessage(rsvp),
|
||||||
|
Title: buildTitle(rsvp),
|
||||||
|
Tags: buildTags(rsvp),
|
||||||
|
Priority: gotfy.Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Ntfy) PublishNewRsvpNotification(rsvp *Rsvp) (string, error) {
|
||||||
|
resp, err := n.client.SendMessage(context.Background(), BuildNtfyMessage("collinenlucy_nl", rsvp))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.ID, nil
|
||||||
|
}
|
||||||
75
server/ntfy_test.go
Normal file
75
server/ntfy_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBuildNtfyMessage(t *testing.T) {
|
||||||
|
rsvp := &Rsvp{
|
||||||
|
Id: 1,
|
||||||
|
Attending: false,
|
||||||
|
PartySize: 1,
|
||||||
|
PartyMembers: []Member{
|
||||||
|
{Name: "test", Child: false, DietaryPreferences: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msg := BuildNtfyMessage("test", rsvp)
|
||||||
|
|
||||||
|
if msg.Topic != "test" {
|
||||||
|
t.Fatal("message topic is incorrect")
|
||||||
|
}
|
||||||
|
if msg.Title != "Someone can't make it" {
|
||||||
|
t.Fatal("message title is incorrect")
|
||||||
|
}
|
||||||
|
if msg.Message != "test\ncan't make it" {
|
||||||
|
t.Fatal("message message is incorrect")
|
||||||
|
}
|
||||||
|
if len(msg.Tags) != 1 || msg.Tags[0] != "x" {
|
||||||
|
t.Fatal("message tags are incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
rsvp = &Rsvp{
|
||||||
|
Id: 1,
|
||||||
|
Attending: true,
|
||||||
|
PartySize: 1,
|
||||||
|
PartyMembers: []Member{
|
||||||
|
{Name: "test", Child: false, DietaryPreferences: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msg = BuildNtfyMessage("test", rsvp)
|
||||||
|
|
||||||
|
if msg.Topic != "test" {
|
||||||
|
t.Fatal("message topic is incorrect")
|
||||||
|
}
|
||||||
|
if msg.Title != "1 person confirmed!" {
|
||||||
|
t.Fatal("message title is incorrect")
|
||||||
|
}
|
||||||
|
if msg.Message != "Here's who is coming 👇\n- 🧓 test, n/a" {
|
||||||
|
t.Fatalf("message message is incorrect")
|
||||||
|
}
|
||||||
|
if len(msg.Tags) != 2 || msg.Tags[0] != "white_check_mark" || msg.Tags[1] != "one" {
|
||||||
|
t.Fatal("message tags are incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
rsvp = &Rsvp{
|
||||||
|
Id: 1,
|
||||||
|
Attending: true,
|
||||||
|
PartySize: 2,
|
||||||
|
PartyMembers: []Member{
|
||||||
|
{Name: "test1", Child: false, DietaryPreferences: ""},
|
||||||
|
{Name: "test2", Child: true, DietaryPreferences: "no tobacco"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msg = BuildNtfyMessage("test", rsvp)
|
||||||
|
|
||||||
|
if msg.Topic != "test" {
|
||||||
|
t.Fatal("message topic is incorrect")
|
||||||
|
}
|
||||||
|
if msg.Title != "2 people confirmed!" {
|
||||||
|
t.Fatal("message title is incorrect")
|
||||||
|
}
|
||||||
|
if msg.Message != "Here's who is coming 👇\n- 🧓 test1, n/a\n- 👶 test2, no tobacco" {
|
||||||
|
t.Fatalf("message message is incorrect")
|
||||||
|
}
|
||||||
|
if len(msg.Tags) != 2 || msg.Tags[0] != "white_check_mark" || msg.Tags[1] != "two" {
|
||||||
|
t.Fatal("message tags are incorrect")
|
||||||
|
}
|
||||||
|
}
|
||||||
117
server/rsvp.go
Normal file
117
server/rsvp.go
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Member struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DietaryPreferences string `json:"dietaryPreferences"`
|
||||||
|
Child bool `json:"child"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rsvp struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Attending bool `json:"attending"`
|
||||||
|
PartySize int64 `json:"partySize"`
|
||||||
|
PartyMembers []Member `json:"partyMembers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupRsvpsTable(db *sql.DB) {
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS rsvps (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
attending BOOLEAN NOT NULL,
|
||||||
|
partySize INTEGER NOT NULL,
|
||||||
|
partyMembers BLOB NOT NULL
|
||||||
|
);`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create rsvp table: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRsvps(db *sql.DB) ([]Rsvp, error) {
|
||||||
|
rows, err := db.Query("SELECT * FROM rsvps ORDER BY id DESC;")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rsvps := make([]Rsvp, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
var attending bool
|
||||||
|
var partySize int64
|
||||||
|
var rawPartyMembers []byte
|
||||||
|
|
||||||
|
err = rows.Scan(&id, &attending, &partySize, &rawPartyMembers)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var members []Member
|
||||||
|
err := json.Unmarshal(rawPartyMembers, &members)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rsvps = append(rsvps, Rsvp{
|
||||||
|
Id: id,
|
||||||
|
Attending: attending,
|
||||||
|
PartySize: partySize,
|
||||||
|
PartyMembers: members,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsvps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rsvp *Rsvp) CreateRsvp(db *sql.DB) (int64, error) {
|
||||||
|
partyMembersBytes, err := json.Marshal(rsvp.PartyMembers)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n", partyMembersBytes)
|
||||||
|
|
||||||
|
result, err := db.Exec(
|
||||||
|
"INSERT INTO rsvps (attending, partySize, partyMembers) VALUES (?, ?, ?);",
|
||||||
|
rsvp.Attending,
|
||||||
|
rsvp.PartySize,
|
||||||
|
partyMembersBytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rsvp.Id = id
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RsvpPublisher interface {
|
||||||
|
PublishNewRsvpNotification(rsvp *Rsvp) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendRsvpNotification(publisher RsvpPublisher, rsvp *Rsvp) {
|
||||||
|
resp, err := publisher.PublishNewRsvpNotification(rsvp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed publishing notification: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("published notification: %s\n", resp)
|
||||||
|
}
|
||||||
169
server/rsvp_test.go
Normal file
169
server/rsvp_test.go
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sqlmock "github.com/DATA-DOG/go-sqlmock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockPublisher struct {
|
||||||
|
timesCalled int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPublisher) PublishNewRsvpNotification(rsvp *Rsvp) (string, error) {
|
||||||
|
m.timesCalled++
|
||||||
|
return "test", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendRsvpNotification(t *testing.T) {
|
||||||
|
rsvp := &Rsvp{
|
||||||
|
Id: 1,
|
||||||
|
Attending: true,
|
||||||
|
PartySize: 1,
|
||||||
|
PartyMembers: []Member{
|
||||||
|
{Name: "test", Child: false, DietaryPreferences: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mp := &mockPublisher{
|
||||||
|
timesCalled: 0,
|
||||||
|
}
|
||||||
|
SendRsvpNotification(mp, rsvp)
|
||||||
|
|
||||||
|
if mp.timesCalled != 1 {
|
||||||
|
t.Fatalf("failed to call PublishNewRsvpNotification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshal(t *testing.T) {
|
||||||
|
var blob = []byte(`{"id":1,"attending":true,"partySize":2,"partyMembers":[{"name":"test1","child":false,"dietaryPreferences":"none"},{"name":"test2","child":true,"dietaryPreferences":"some"}]}`)
|
||||||
|
var rsvp Rsvp
|
||||||
|
err := json.Unmarshal(blob, &rsvp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsvp.Id != 1 {
|
||||||
|
t.Fatal("failed to unmarshal id")
|
||||||
|
}
|
||||||
|
if !rsvp.Attending {
|
||||||
|
t.Fatal("failed to unmarshal attending")
|
||||||
|
}
|
||||||
|
if rsvp.PartySize != 2 {
|
||||||
|
t.Fatal("failed to unmarshal partySize")
|
||||||
|
}
|
||||||
|
if len(rsvp.PartyMembers) != 2 || rsvp.PartyMembers[0].Name != "test1" || rsvp.PartyMembers[0].Child || rsvp.PartyMembers[0].DietaryPreferences != "none" || rsvp.PartyMembers[1].Name != "test2" || !rsvp.PartyMembers[1].Child || rsvp.PartyMembers[1].DietaryPreferences != "some" {
|
||||||
|
t.Fatal("failed to unmarshal partyMembers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshal(t *testing.T) {
|
||||||
|
rsvp := Rsvp{
|
||||||
|
Id: 1,
|
||||||
|
Attending: true,
|
||||||
|
PartySize: 2,
|
||||||
|
PartyMembers: []Member{
|
||||||
|
{Name: "test1", Child: false, DietaryPreferences: "none"},
|
||||||
|
{Name: "test2", Child: true, DietaryPreferences: "some"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
marshalled, err := json.Marshal(rsvp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to marshal")
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected = []byte(`{"id":1,"attending":true,"partySize":2,"partyMembers":[{"name":"test1","dietaryPreferences":"none","child":false},{"name":"test2","dietaryPreferences":"some","child":true}]}`)
|
||||||
|
if !bytes.Equal(marshalled, expected) {
|
||||||
|
t.Fatalf("failed to produce matching bytes,\ngot:\t\t\t%s\nexpected:\t%s\n", marshalled, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRsvps(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to create mock database")
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := []string{"id", "attending", "party_size", "party_members"}
|
||||||
|
mock.ExpectQuery(
|
||||||
|
"SELECT \\* FROM rsvps ORDER BY id DESC;",
|
||||||
|
).
|
||||||
|
WillReturnRows(
|
||||||
|
sqlmock.
|
||||||
|
NewRows(columns).
|
||||||
|
AddRow(
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
2,
|
||||||
|
[]byte(`[{"name":"test1","child":false,"dietaryPreferences":"none"},{"name":"test2","child":true,"dietaryPreferences":"some"}]`),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
rsvps, err := GetRsvps(db)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rsvps) != 1 {
|
||||||
|
t.Fatal("didn't receive 1 rsvp")
|
||||||
|
}
|
||||||
|
|
||||||
|
rsvp := rsvps[0]
|
||||||
|
|
||||||
|
if rsvp.Id != 1 {
|
||||||
|
t.Fatal("incorrect id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rsvp.Attending {
|
||||||
|
t.Fatal("incorrect attending")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsvp.PartySize != 2 {
|
||||||
|
t.Fatal("incorrect party_size")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rsvp.PartyMembers) != 2 {
|
||||||
|
t.Fatal("incorrect party_members length ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsvp.PartyMembers[0].Name != "test1" || rsvp.PartyMembers[0].Child || rsvp.PartyMembers[0].DietaryPreferences != "none" {
|
||||||
|
t.Fatal("incorrect party_members[0]")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsvp.PartyMembers[1].Name != "test2" || !rsvp.PartyMembers[1].Child || rsvp.PartyMembers[1].DietaryPreferences != "some" {
|
||||||
|
t.Fatal("incorrect party_members[1]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRsvp(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to create mock database")
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.ExpectExec(
|
||||||
|
"INSERT INTO rsvps \\(attending, partySize, partyMembers\\) VALUES \\(\\?, \\?, \\?\\);",
|
||||||
|
).
|
||||||
|
WithArgs(true, 2, []byte(`[{"name":"test","dietaryPreferences":"none","child":false}]`)).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
rsvp := Rsvp{
|
||||||
|
Id: -1,
|
||||||
|
Attending: true,
|
||||||
|
PartySize: 2,
|
||||||
|
PartyMembers: []Member{
|
||||||
|
{Name: "test", Child: false, DietaryPreferences: "none"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
id, err := rsvp.CreateRsvp(db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != 1 || rsvp.Id != 1 {
|
||||||
|
t.Fatal("received wrong id")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue