Add basic event logging
This commit is contained in:
parent
192da1a74f
commit
86c1e4fea3
|
|
@ -23,6 +23,8 @@ func (a *Admin) CreateGroup(name string, reference string) (*Group, error)
|
||||||
|
|
||||||
func (*Admin) GetUser(id string) (*User, error)
|
func (*Admin) GetUser(id string) (*User, error)
|
||||||
|
|
||||||
|
func (a *Admin) ListEvents() ([]Event, error)
|
||||||
|
|
||||||
func (a *Admin) ListGroups() ([]Group, error)
|
func (a *Admin) ListGroups() ([]Group, error)
|
||||||
|
|
||||||
func (*Admin) ListUsers() ([]User, error)
|
func (*Admin) ListUsers() ([]User, error)
|
||||||
|
|
@ -37,6 +39,15 @@ func (u *Admin) UserSetLive(userReference string, setting bool) error
|
||||||
|
|
||||||
type ErrorInvalidCredentials error
|
type ErrorInvalidCredentials error
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Id string
|
||||||
|
ActorId string
|
||||||
|
ActionType string
|
||||||
|
TargetType string
|
||||||
|
TargetId string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
Id string
|
Id string
|
||||||
Name string
|
Name string
|
||||||
|
|
@ -72,7 +83,7 @@ func (s *Session) GetOthersWishes(userReference string) ([]Wish, error)
|
||||||
|
|
||||||
func (s *Session) GetWishes() ([]Wish, error)
|
func (s *Session) GetWishes() ([]Wish, error)
|
||||||
|
|
||||||
func (s *Session) MakeWish(name string) error
|
func (s *Session) MakeWish(name string) (string, error)
|
||||||
|
|
||||||
func (s *Session) RecindWishesForUser(ids ...string) error
|
func (s *Session) RecindWishesForUser(ids ...string) error
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
package lishwist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"lishwist/core/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventActionCreate = "CREATE"
|
||||||
|
eventActionHide = "HIDE"
|
||||||
|
eventActionUnhide = "UNHIDE"
|
||||||
|
eventActionClaim = "CLAIM"
|
||||||
|
eventActionUnclaim = "UNCLAIM"
|
||||||
|
eventActionComplete = "COMPLETE"
|
||||||
|
// eventActionDelete = "DELETE" NOTE: We can't have this, because there'll be no target to reference
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventTargetGroup = "GROUP"
|
||||||
|
eventTargetUser = "USER"
|
||||||
|
eventTargetWish = "WISH"
|
||||||
|
eventTargetGroupMember = "GROUP_MEMBER"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Id string
|
||||||
|
ActorId string
|
||||||
|
ActionType string
|
||||||
|
TargetType string
|
||||||
|
TargetId string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// type EventCreateGroupMember struct {
|
||||||
|
// Event
|
||||||
|
// Actor User
|
||||||
|
// User
|
||||||
|
// Group
|
||||||
|
// }
|
||||||
|
|
||||||
|
func queryManyEvents(query string, args ...any) ([]Event, error) {
|
||||||
|
rows, err := db.Connection.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
events := []Event{}
|
||||||
|
for rows.Next() {
|
||||||
|
var g Event
|
||||||
|
var createdAt string
|
||||||
|
err = rows.Scan(&g.Id, &g.ActorId, &g.ActionType, &g.TargetType, &g.TargetId, &createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
g.CreatedAt, err = time.Parse(time.RFC3339Nano, createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse created_at: %w", err)
|
||||||
|
}
|
||||||
|
events = append(events, g)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryOneEvent(query string, args ...any) (*Event, error) {
|
||||||
|
events, err := queryManyEvents(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(events) < 1 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &events[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admin) ListEvents() ([]Event, error) {
|
||||||
|
query := "SELECT id, actor_id, action_type, target_type, target_id, created_at FROM event;"
|
||||||
|
return queryManyEvents(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEvent(actorId, actionType, targetType string, targetIds ...string) {
|
||||||
|
// TODO: If this were to accept sql.Tx it could be used in atomic transactions
|
||||||
|
numTargets := len(targetIds)
|
||||||
|
if numTargets < 1 {
|
||||||
|
log.Println("Warning: recordEvent called with no target IDs. Skipping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stmt := "INSERT INTO event (actor_id, action_type, target_type, target_id) VALUES (?, ?, ?, ?)"
|
||||||
|
extraValuePlaceholders := strings.Repeat(", (?, ?, ?, ?)", numTargets-1)
|
||||||
|
args := make([]any, numTargets*4)
|
||||||
|
for i, id := range targetIds {
|
||||||
|
args[i*4] = actorId
|
||||||
|
args[i*4+1] = actionType
|
||||||
|
args[i*4+2] = targetType
|
||||||
|
args[i*4+3] = id
|
||||||
|
}
|
||||||
|
_, err := db.Connection.Exec(stmt+extraValuePlaceholders, args...)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if numTargets == 1 {
|
||||||
|
log.Printf("Failed to record %s %s event: failed to execute query: %s\n", actionType, targetType, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Failed to record %d %s %s events: failed to execute query: %s\n", numTargets, actionType, targetType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEventCreateGroup(actorId, groupId string) {
|
||||||
|
recordEvent(actorId, eventActionCreate, eventTargetGroup, groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEventCreateUser(actorId, userId string) {
|
||||||
|
recordEvent(actorId, eventActionCreate, eventTargetUser, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEventCreateWish(actorId, wishId string) {
|
||||||
|
recordEvent(actorId, eventActionCreate, eventTargetWish, wishId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEventCreateGroupMember(actorId, groupMemberId string) {
|
||||||
|
recordEvent(actorId, eventActionCreate, eventTargetGroupMember, groupMemberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: I can't use these yet because the associated actions use reference
|
||||||
|
// func recordEventHideUser(actorId, userId string) {
|
||||||
|
// recordEvent(actorId, eventActionHide, eventTargetUser, userId)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func recordEventUnhideUser(actorId, userId string) {
|
||||||
|
// recordEvent(actorId, eventActionUnhide, eventTargetUser, userId)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func recordEventClaimWishes(actorId string, wishIds ...string) {
|
||||||
|
recordEvent(actorId, eventActionClaim, eventTargetWish, wishIds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEventUnclaimWishes(actorId string, wishIds ...string) {
|
||||||
|
recordEvent(actorId, eventActionUnclaim, eventTargetWish, wishIds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordEventCompleteWishes(actorId string, wishIds ...string) {
|
||||||
|
recordEvent(actorId, eventActionComplete, eventTargetWish, wishIds...)
|
||||||
|
}
|
||||||
|
|
@ -103,15 +103,21 @@ func (a *Admin) CreateGroup(name string, reference string) (*Group, error) {
|
||||||
Name: name,
|
Name: name,
|
||||||
Reference: reference,
|
Reference: reference,
|
||||||
}
|
}
|
||||||
|
recordEventCreateGroup(a.session.user.Id, group.Id)
|
||||||
return &group, nil
|
return &group, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Admin) AddUserToGroup(userId, groupId string) error {
|
func (a *Admin) AddUserToGroup(userId, groupId string) error {
|
||||||
stmt := "INSERT INTO group_member (user_id, group_id) VALUES (?, ?)"
|
stmt := "INSERT INTO group_member (user_id, group_id) VALUES (?, ?)"
|
||||||
_, err := db.Connection.Exec(stmt, userId, groupId)
|
result, err := db.Connection.Exec(stmt, userId, groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("query execution failed: %w", err)
|
||||||
}
|
}
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get last insert id: %w", err)
|
||||||
|
}
|
||||||
|
recordEventCreateGroupMember(a.session.user.Id, strconv.FormatInt(id, 10))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,12 @@ func TestCreateGroup(t *testing.T) {
|
||||||
|
|
||||||
fixtures.AssertEq(t, "Number of users", "My Friends", group.Name)
|
fixtures.AssertEq(t, "Number of users", "My Friends", group.Name)
|
||||||
fixtures.AssertEq(t, "Number of users", "my-friends", group.Reference)
|
fixtures.AssertEq(t, "Number of users", "my-friends", group.Reference)
|
||||||
|
|
||||||
|
// FIXME: disabled for now because datetimes break this
|
||||||
|
// events, err := s.Admin().ListEvents()
|
||||||
|
// assert.FatalErr(t, "listing events", err)
|
||||||
|
|
||||||
|
// assert.JsonSnapshot(t, "TestCreateGroup.snap.txt", events)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCantSeeSelfInGroup(t *testing.T) {
|
func TestCantSeeSelfInGroup(t *testing.T) {
|
||||||
|
|
@ -39,4 +45,10 @@ func TestCantSeeSelfInGroup(t *testing.T) {
|
||||||
fixtures.AssertEq(t, "Group contains 2 users", 2, len(group.Members))
|
fixtures.AssertEq(t, "Group contains 2 users", 2, len(group.Members))
|
||||||
fixtures.AssertEq(t, "Group user 1 is thomas", "thomas", group.Members[0].Name)
|
fixtures.AssertEq(t, "Group user 1 is thomas", "thomas", group.Members[0].Name)
|
||||||
fixtures.AssertEq(t, "Group user 2 is caleb", "caleb", group.Members[1].Name)
|
fixtures.AssertEq(t, "Group user 2 is caleb", "caleb", group.Members[1].Name)
|
||||||
|
|
||||||
|
// FIXME: disabled for now because datetimes break this
|
||||||
|
// events, err := s.Admin().ListEvents()
|
||||||
|
// assert.FatalErr(t, "listing events", err)
|
||||||
|
|
||||||
|
// assert.JsonSnapshot(t, "TestCantSeeSelfInGroup.snap.txt", events)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package assert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Eq[C comparable](t *testing.T, context string, expected, actual C) {
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func True(t *testing.T, context string, condition bool) {
|
||||||
|
if !condition {
|
||||||
|
t.Errorf("false: %s", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalTrue(t *testing.T, context string, condition bool) {
|
||||||
|
if !condition {
|
||||||
|
t.Fatalf("%s", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalErr(t *testing.T, context string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %s", context, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalErrIs(t *testing.T, context string, err, target error) {
|
||||||
|
if !errors.Is(err, target) {
|
||||||
|
t.Fatalf("%s: encountered unexpected error: %s", context, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FatalErrAs(t *testing.T, context string, err error, target any) {
|
||||||
|
if !errors.As(err, target) {
|
||||||
|
t.Fatalf("%s: encountered unexpected error: %s", context, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SlicesEq[S ~[]E, E comparable](t *testing.T, context string, expected, actual S) {
|
||||||
|
if !slices.Equal(expected, actual) {
|
||||||
|
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package assert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return !errors.Is(err, os.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSnapshot(t *testing.T, path, actual string) {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create snapshot file: %s", err)
|
||||||
|
}
|
||||||
|
_, err = f.Write([]byte(actual))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write to snapshot file: %s", err)
|
||||||
|
}
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to close snapshot file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TextSnapshot(t *testing.T, path, actual string) {
|
||||||
|
if !pathExists(path) {
|
||||||
|
writeSnapshot(t, path, actual)
|
||||||
|
t.Errorf("Snapshot file created: %s", path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read snapshot file: %s", err)
|
||||||
|
}
|
||||||
|
expected := string(content)
|
||||||
|
|
||||||
|
if expected != actual {
|
||||||
|
t.Errorf("Value doesn't match snapshot %s:\n%s", path, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("UPDATE_SNAPSHOTS") == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSnapshot(t, path, actual)
|
||||||
|
fmt.Printf("Snapshot file %s updated\n", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func JsonSnapshot(t *testing.T, path string, actual any) {
|
||||||
|
data, err := json.MarshalIndent(actual, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Snapshot failed to serialize actual to JSON: %s", err)
|
||||||
|
}
|
||||||
|
TextSnapshot(t, path, string(data))
|
||||||
|
}
|
||||||
|
|
@ -30,8 +30,10 @@ CREATE TABLE IF NOT EXISTS "group" (
|
||||||
PRIMARY KEY("id" AUTOINCREMENT)
|
PRIMARY KEY("id" AUTOINCREMENT)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "group_member" (
|
CREATE TABLE IF NOT EXISTS "group_member" (
|
||||||
|
"id" INTEGER NOT NULL UNIQUE,
|
||||||
"group_id" INTEGER NOT NULL,
|
"group_id" INTEGER NOT NULL,
|
||||||
"user_id" INTEGER NOT NULL,
|
"user_id" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY("id" AUTOINCREMENT),
|
||||||
UNIQUE("user_id","group_id"),
|
UNIQUE("user_id","group_id"),
|
||||||
FOREIGN KEY("group_id") REFERENCES "group"("id"),
|
FOREIGN KEY("group_id") REFERENCES "group"("id"),
|
||||||
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
||||||
|
|
@ -44,6 +46,15 @@ CREATE TABLE IF NOT EXISTS "session" (
|
||||||
PRIMARY KEY("id" AUTOINCREMENT),
|
PRIMARY KEY("id" AUTOINCREMENT),
|
||||||
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "event" (
|
||||||
|
"id" INTEGER NOT NULL UNIQUE,
|
||||||
|
"actor_id" INTEGER NOT NULL,
|
||||||
|
"action_type" TEXT NOT NULL,
|
||||||
|
"target_type" TEXT NOT NULL,
|
||||||
|
"target_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ')),
|
||||||
|
PRIMARY KEY("id" AUTOINCREMENT)
|
||||||
|
);
|
||||||
|
|
||||||
DROP VIEW IF EXISTS "v_user";
|
DROP VIEW IF EXISTS "v_user";
|
||||||
CREATE VIEW "v_user"
|
CREATE VIEW "v_user"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE group_member RENAME TO old_group_member;
|
||||||
|
|
||||||
|
CREATE TABLE "group_member" (
|
||||||
|
"id" INTEGER NOT NULL UNIQUE,
|
||||||
|
"group_id" INTEGER NOT NULL,
|
||||||
|
"user_id" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY("id" AUTOINCREMENT),
|
||||||
|
UNIQUE("user_id","group_id"),
|
||||||
|
FOREIGN KEY("group_id") REFERENCES "group"("id"),
|
||||||
|
FOREIGN KEY("user_id") REFERENCES "user"("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO group_member (group_id, user_id) SELECT group_id, user_id FROM old_group_member;
|
||||||
|
|
||||||
|
DROP TABLE "old_group_member";
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -2,24 +2,28 @@ package fixtures
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
// Deprecated: use internal/assert
|
||||||
func AssertEq[C comparable](t *testing.T, context string, expected, actual C) {
|
func AssertEq[C comparable](t *testing.T, context string, expected, actual C) {
|
||||||
if expected != actual {
|
if expected != actual {
|
||||||
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
t.Errorf("%s: %#v != %#v", context, expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: use internal/assert
|
||||||
func Assert(t *testing.T, context string, condition bool) {
|
func Assert(t *testing.T, context string, condition bool) {
|
||||||
if !condition {
|
if !condition {
|
||||||
t.Errorf("%s", context)
|
t.Errorf("%s", context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: use internal/assert
|
||||||
func FatalAssert(t *testing.T, context string, condition bool) {
|
func FatalAssert(t *testing.T, context string, condition bool) {
|
||||||
if !condition {
|
if !condition {
|
||||||
t.Fatalf("%s", context)
|
t.Fatalf("%s", context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: use internal/assert
|
||||||
func FailIfErr(t *testing.T, err error, context string) {
|
func FailIfErr(t *testing.T, err error, context string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s: %s\n", context, err)
|
t.Fatalf("%s: %s\n", context, err)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package fixtures
|
package fixtures
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -15,21 +14,36 @@ func TestInit(t *testing.T) error {
|
||||||
return lishwist.Init(uri)
|
return lishwist.Init(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: This function also inits the test, which prevents it from being used more than once per test
|
||||||
func Login(t *testing.T, username, password string) *lishwist.Session {
|
func Login(t *testing.T, username, password string) *lishwist.Session {
|
||||||
uri := memdb.TestDB(t)
|
uri := memdb.TestDB(t)
|
||||||
err := lishwist.Init(uri)
|
err := lishwist.Init(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to init db: %s\n", err)
|
t.Fatalf("Failed to init db: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = lishwist.Register(username, password)
|
_, err = lishwist.Register(username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to register on login fixture: %s\n", err)
|
t.Fatalf("Failed to register on login fixture: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := lishwist.Login(username, password, time.Hour*24)
|
session, err := lishwist.Login(username, password, time.Hour*24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to login on fixture: %s\n", err)
|
t.Fatalf("Failed to login on fixture: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func Login2(t *testing.T, username, password string) *lishwist.Session {
|
||||||
|
_, err := lishwist.Register(username, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to register on login fixture: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := lishwist.Login(username, password, time.Hour*24)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to login on fixture: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ func createUser(name string, passHash []byte, isAdmin bool) (*User, error) {
|
||||||
Id: fmt.Sprintf("%d", id),
|
Id: fmt.Sprintf("%d", id),
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
|
recordEventCreateUser(user.Id, user.Id)
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
44
core/wish.go
44
core/wish.go
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"lishwist/core/internal/db"
|
"lishwist/core/internal/db"
|
||||||
|
|
@ -52,13 +53,20 @@ func (s *Session) GetWishes() ([]Wish, error) {
|
||||||
return wishs, nil
|
return wishs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) MakeWish(name string) error {
|
// May return the id of the wish
|
||||||
|
func (s *Session) MakeWish(name string) (string, error) {
|
||||||
stmt := "INSERT INTO wish (name, recipient_id, creator_id) VALUES (?, ?, ?)"
|
stmt := "INSERT INTO wish (name, recipient_id, creator_id) VALUES (?, ?, ?)"
|
||||||
_, err := db.Connection.Exec(stmt, strings.TrimSpace(name), s.User().Id, s.User().Id)
|
result, err := db.Connection.Exec(stmt, strings.TrimSpace(name), s.User().Id, s.User().Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Query execution failed: %w", err)
|
return "", fmt.Errorf("Query execution failed: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get last insert id: %w", err)
|
||||||
|
}
|
||||||
|
wishId := strconv.FormatInt(id, 10)
|
||||||
|
recordEventCreateWish(s.user.Id, wishId)
|
||||||
|
return wishId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) deleteWishes(tx *sql.Tx, ids []string) error {
|
func (s *Session) deleteWishes(tx *sql.Tx, ids []string) error {
|
||||||
|
|
@ -194,7 +202,10 @@ func (s *Session) executeClaims(tx *sql.Tx, claims, unclaims []string) error {
|
||||||
|
|
||||||
// Undertake or abandon wishes made by other users
|
// Undertake or abandon wishes made by other users
|
||||||
func (s *Session) ClaimWishes(claims, unclaims []string) error {
|
func (s *Session) ClaimWishes(claims, unclaims []string) error {
|
||||||
if len(claims) < 1 && len(unclaims) < 1 {
|
lenClaims := len(claims)
|
||||||
|
lenUnclaims := len(unclaims)
|
||||||
|
// TODO: Would be nice if this used a request builder
|
||||||
|
if lenClaims < 1 && lenUnclaims < 1 {
|
||||||
return fmt.Errorf("Attempt to claim/unclaim zero wishes")
|
return fmt.Errorf("Attempt to claim/unclaim zero wishes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,8 +224,18 @@ func (s *Session) ClaimWishes(claims, unclaims []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// TODO: This could be atomic. See recordEvent function
|
||||||
|
if lenClaims > 0 {
|
||||||
|
recordEventClaimWishes(s.user.Id, claims...)
|
||||||
|
}
|
||||||
|
if lenUnclaims > 0 {
|
||||||
|
recordEventUnclaimWishes(s.user.Id, unclaims...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func executeCompletions(tx *sql.Tx, claims []string) error {
|
func executeCompletions(tx *sql.Tx, claims []string) error {
|
||||||
claimStmt := "UPDATE wish SET sent = 1 WHERE id = ?"
|
claimStmt := "UPDATE wish SET sent = 1 WHERE id = ?"
|
||||||
|
|
@ -234,8 +255,8 @@ func executeCompletions(tx *sql.Tx, claims []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: User ought not be able to interact with wishes outside their group network
|
|
||||||
func (s *Session) CompleteWishes(claims []string) error {
|
func (s *Session) CompleteWishes(claims []string) error {
|
||||||
|
// TODO: User ought not be able to interact with wishes outside their group network
|
||||||
if len(claims) < 1 {
|
if len(claims) < 1 {
|
||||||
return fmt.Errorf("Attempt to complete zero wishes")
|
return fmt.Errorf("Attempt to complete zero wishes")
|
||||||
}
|
}
|
||||||
|
|
@ -255,9 +276,15 @@ func (s *Session) CompleteWishes(claims []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordEventCompleteWishes(s.user.Id, claims...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *Session) SuggestWishForUser(otherUserReference string, wishName string) error {
|
func (u *Session) SuggestWishForUser(otherUserReference string, wishName string) error {
|
||||||
otherUser, err := GetUserByReference(otherUserReference)
|
otherUser, err := GetUserByReference(otherUserReference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -291,5 +318,8 @@ func (s *Session) RecindWishesForUser(ids ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,18 @@ package lishwist_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"lishwist/core/internal/assert"
|
||||||
"lishwist/core/internal/fixtures"
|
"lishwist/core/internal/fixtures"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMakeWish(t *testing.T) {
|
func TestMakeWish(t *testing.T) {
|
||||||
s := fixtures.Login(t, "thomas", "123")
|
s := fixtures.Login(t, "thomas", "123")
|
||||||
|
|
||||||
if err := s.MakeWish("apple"); err != nil {
|
if _, err := s.MakeWish("apple"); err != nil {
|
||||||
t.Fatalf("Failed to make wish 1: %s\n", err)
|
t.Fatalf("Failed to make wish 1: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.MakeWish(" A car "); err != nil {
|
if _, err := s.MakeWish(" A car "); err != nil {
|
||||||
t.Fatalf("Failed to make wish 2: %s\n", err)
|
t.Fatalf("Failed to make wish 2: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,3 +27,35 @@ func TestMakeWish(t *testing.T) {
|
||||||
fixtures.AssertEq(t, "Wish 1 name", wishes[0].Name, "apple")
|
fixtures.AssertEq(t, "Wish 1 name", wishes[0].Name, "apple")
|
||||||
fixtures.AssertEq(t, "Wish 2 name", wishes[1].Name, "A car")
|
fixtures.AssertEq(t, "Wish 2 name", wishes[1].Name, "A car")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClaimUnclaimWishes(t *testing.T) {
|
||||||
|
err := fixtures.TestInit(t)
|
||||||
|
assert.FatalErr(t, "initializing db", err)
|
||||||
|
|
||||||
|
thomas := fixtures.Login2(t, "thomas", "123")
|
||||||
|
assert.FatalErr(t, "registering thomas", err)
|
||||||
|
|
||||||
|
caleb := fixtures.Login2(t, "caleb", "123")
|
||||||
|
assert.FatalErr(t, "registering caleb", err)
|
||||||
|
|
||||||
|
food, err := caleb.MakeWish("food")
|
||||||
|
assert.FatalErr(t, "making wish 1", err)
|
||||||
|
|
||||||
|
box, err := caleb.MakeWish("box")
|
||||||
|
assert.FatalErr(t, "making wish 2", err)
|
||||||
|
|
||||||
|
drink, err := caleb.MakeWish("drink")
|
||||||
|
assert.FatalErr(t, "making wish 3", err)
|
||||||
|
|
||||||
|
err = thomas.ClaimWishes([]string{food, box, drink}, []string{})
|
||||||
|
assert.FatalErr(t, "claiming wishes", err)
|
||||||
|
|
||||||
|
err = thomas.ClaimWishes([]string{}, []string{food, box, drink})
|
||||||
|
assert.FatalErr(t, "unclaiming wishes", err)
|
||||||
|
|
||||||
|
// FIXME: disabled for now because datetimes break this
|
||||||
|
// events, err := thomas.Admin().ListEvents()
|
||||||
|
// assert.FatalErr(t, "listing events", err)
|
||||||
|
|
||||||
|
// assert.JsonSnapshot(t, "TestClaimUnclaimWishes.snap.txt", events)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
top_level=$(git rev-parse --show-toplevel)
|
top_level=$(git rev-parse --show-toplevel)
|
||||||
git_version=$($top_level/scripts/git-version)
|
git_version=$($top_level/scripts/git-version)
|
||||||
|
|
||||||
|
go generate ../core/internal/db
|
||||||
go run -ldflags=-X=lishwist/http/env.GitVersion=$git_version main.go
|
go run -ldflags=-X=lishwist/http/env.GitVersion=$git_version main.go
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
lishwist "lishwist/core"
|
||||||
|
"lishwist/http/response"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Teajey/rsvp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Events struct {
|
||||||
|
Events []lishwist.Event `xml:"Event"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func EventList(app *lishwist.Session, session *response.Session, h http.Header, r *http.Request) rsvp.Response {
|
||||||
|
admin := app.Admin()
|
||||||
|
if admin == nil {
|
||||||
|
log.Println("Attempt to access EventList by non-admin. Responding 404 Not Found.")
|
||||||
|
return response.NotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := admin.ListEvents()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Admin failed to ListEvents: %s\n", err)
|
||||||
|
return response.Error(http.StatusInternalServerError, "Failed to get events: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data("", Events{Events: events})
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ func WishlistAdd(app *lishwist.Session, h http.Header, r *http.Request) rsvp.Res
|
||||||
}
|
}
|
||||||
|
|
||||||
newGiftName := r.Form.Get("gift_name")
|
newGiftName := r.Form.Get("gift_name")
|
||||||
err = app.MakeWish(newGiftName)
|
_, err = app.MakeWish(newGiftName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%s\n", err)
|
log.Printf("%s\n", err)
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to add gift.")
|
return response.Error(http.StatusInternalServerError, "Failed to add gift.")
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ func Create(useSecureCookies bool) *router.VisibilityRouter {
|
||||||
r.Public.HandleFunc("POST /", routing.LoginPost)
|
r.Public.HandleFunc("POST /", routing.LoginPost)
|
||||||
r.Public.HandleFunc("POST /register", routing.RegisterPost)
|
r.Public.HandleFunc("POST /register", routing.RegisterPost)
|
||||||
|
|
||||||
|
r.Private.HandleFunc("GET /events", routing.ExpectAppSession(routing.EventList))
|
||||||
r.Private.HandleFunc("GET /account", routing.ExpectAppSession(routing.Account))
|
r.Private.HandleFunc("GET /account", routing.ExpectAppSession(routing.Account))
|
||||||
r.Private.HandleFunc("GET /health", routing.ExpectAppSession(routing.Health))
|
r.Private.HandleFunc("GET /health", routing.ExpectAppSession(routing.Health))
|
||||||
r.Private.HandleFunc("GET /", routing.NotFound)
|
r.Private.HandleFunc("GET /", routing.NotFound)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue