From 63ff9c3fa94c1b79f2c765ec89f090e5997d5aa1 Mon Sep 17 00:00:00 2001 From: Joshua Herring Date: Wed, 27 May 2026 12:58:12 -0400 Subject: [PATCH] add CRUD for Activity --- dbi/activities.go | 87 +++++++++++++++++ dbi/activities_test.go | 206 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 dbi/activities.go create mode 100644 dbi/activities_test.go diff --git a/dbi/activities.go b/dbi/activities.go new file mode 100644 index 0000000..2a457a3 --- /dev/null +++ b/dbi/activities.go @@ -0,0 +1,87 @@ +package dbi + +import ( + "context" + "database/sql" + "fmt" +) + +type Activity struct { + UID string `json:"uid"` + Title string `json:"title"` + Description string `json:"description"` + Hyperlink string `json:"hyperlink"` + Status string `json:"status"` + Created string `json:"created"` + Modified string `json:"modified"` + Username string `json:"username"` +} + +func CreateActivity(conn *sql.Conn, a *Activity) error { + if a.UID == "" { + a.UID = GenUUID() + } + now := GetNow() + a.Created = now + a.Modified = now + _, err := conn.ExecContext(context.Background(), + `INSERT INTO activities (UID, Title, Description, Hyperlink, Status, Created, Modified, Username) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + a.UID, a.Title, a.Description, a.Hyperlink, a.Status, a.Created, a.Modified, a.Username, + ) + return err +} + +func GetActivity(conn *sql.Conn, uid string) (Activity, error) { + row := conn.QueryRowContext(context.Background(), + `SELECT UID, Title, Description, Hyperlink, Status, Created, Modified, Username + FROM activities WHERE UID = ?`, + uid, + ) + var a Activity + err := row.Scan(&a.UID, &a.Title, &a.Description, &a.Hyperlink, &a.Status, &a.Created, &a.Modified, &a.Username) + if err == sql.ErrNoRows { + return Activity{}, fmt.Errorf("activity %q not found", uid) + } + return a, err +} + +func UpdateActivity(conn *sql.Conn, a *Activity) error { + a.Modified = GetNow() + _, err := conn.ExecContext(context.Background(), + `UPDATE activities SET Title = ?, Description = ?, Hyperlink = ?, Status = ?, Modified = ?, Username = ? + WHERE UID = ?`, + a.Title, a.Description, a.Hyperlink, a.Status, a.Modified, a.Username, a.UID, + ) + return err +} + +func DeleteActivity(conn *sql.Conn, uid string) error { + _, err := conn.ExecContext(context.Background(), + `DELETE FROM activities WHERE UID = ?`, + uid, + ) + return err +} + +func GetActivitiesForUsername(conn *sql.Conn, username string) ([]Activity, error) { + rows, err := conn.QueryContext(context.Background(), + `SELECT UID, Title, Description, Hyperlink, Status, Created, Modified, Username + FROM activities WHERE Username = ?`, + username, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var results []Activity + for rows.Next() { + var a Activity + if err := rows.Scan(&a.UID, &a.Title, &a.Description, &a.Hyperlink, &a.Status, &a.Created, &a.Modified, &a.Username); err != nil { + return nil, err + } + results = append(results, a) + } + return results, rows.Err() +} diff --git a/dbi/activities_test.go b/dbi/activities_test.go new file mode 100644 index 0000000..7313cc4 --- /dev/null +++ b/dbi/activities_test.go @@ -0,0 +1,206 @@ +package dbi + +import ( + "testing" +) + +func genActivity(username string) Activity { + return Activity{ + Title: randString(20), + Description: randString(60), + Hyperlink: "https://" + randString(12) + ".example.com", + Status: "reported", + Username: username, + } +} + +func TestCreateActivity(t *testing.T) { + conn := testConn(t, testDB(t)) + a := genActivity(randString(10)) + + if err := CreateActivity(conn, &a); err != nil { + t.Fatalf("CreateActivity: %v", err) + } + if a.UID == "" { + t.Error("expected UID to be set after create") + } + if a.Created == "" { + t.Error("expected Created to be set after create") + } + if a.Modified == "" { + t.Error("expected Modified to be set after create") + } + + got, err := GetActivity(conn, a.UID) + if err != nil { + t.Fatalf("GetActivity after create: %v", err) + } + if got != a { + t.Errorf("got %+v, want %+v", got, a) + } +} + +func TestCreateActivityPresetUID(t *testing.T) { + conn := testConn(t, testDB(t)) + a := genActivity(randString(10)) + a.UID = "preset" + randString(10) + presetUID := a.UID + + if err := CreateActivity(conn, &a); err != nil { + t.Fatalf("CreateActivity: %v", err) + } + if a.UID != presetUID { + t.Error("CreateActivity should not overwrite a non-empty UID") + } +} + +func TestCreateActivityDuplicateUID(t *testing.T) { + conn := testConn(t, testDB(t)) + a := genActivity(randString(10)) + + if err := CreateActivity(conn, &a); err != nil { + t.Fatalf("first CreateActivity: %v", err) + } + a2 := a + if err := CreateActivity(conn, &a2); err == nil { + t.Error("expected error on duplicate UID, got nil") + } +} + +func TestGetActivity(t *testing.T) { + conn := testConn(t, testDB(t)) + a := genActivity(randString(10)) + + if err := CreateActivity(conn, &a); err != nil { + t.Fatalf("CreateActivity: %v", err) + } + + got, err := GetActivity(conn, a.UID) + if err != nil { + t.Fatalf("GetActivity: %v", err) + } + if got != a { + t.Errorf("got %+v, want %+v", got, a) + } +} + +func TestGetActivityNotFound(t *testing.T) { + conn := testConn(t, testDB(t)) + + _, err := GetActivity(conn, randString(32)) + if err == nil { + t.Error("expected error for missing activity, got nil") + } +} + +func TestUpdateActivity(t *testing.T) { + conn := testConn(t, testDB(t)) + a := genActivity(randString(10)) + + if err := CreateActivity(conn, &a); err != nil { + t.Fatalf("CreateActivity: %v", err) + } + + originalUID := a.UID + originalCreated := a.Created + a.Title = randString(20) + a.Description = randString(60) + a.Hyperlink = "https://" + randString(12) + ".example.com" + a.Status = "posted" + + if err := UpdateActivity(conn, &a); err != nil { + t.Fatalf("UpdateActivity: %v", err) + } + + got, err := GetActivity(conn, a.UID) + if err != nil { + t.Fatalf("GetActivity after update: %v", err) + } + if got.UID != originalUID { + t.Errorf("UID changed: got %q, want %q", got.UID, originalUID) + } + if got.Created != originalCreated { + t.Errorf("Created changed: got %q, want %q", got.Created, originalCreated) + } + if got.Title != a.Title { + t.Errorf("Title not updated: got %q, want %q", got.Title, a.Title) + } + if got.Description != a.Description { + t.Errorf("Description not updated: got %q, want %q", got.Description, a.Description) + } + if got.Status != "posted" { + t.Errorf("Status not updated: got %q", got.Status) + } + if got.Modified == "" { + t.Error("Modified should be set after update") + } +} + +func TestDeleteActivity(t *testing.T) { + conn := testConn(t, testDB(t)) + a := genActivity(randString(10)) + + if err := CreateActivity(conn, &a); err != nil { + t.Fatalf("CreateActivity: %v", err) + } + + if err := DeleteActivity(conn, a.UID); err != nil { + t.Fatalf("DeleteActivity: %v", err) + } + + _, err := GetActivity(conn, a.UID) + if err == nil { + t.Error("expected error after delete, got nil") + } +} + +func TestGetActivitiesForUsername(t *testing.T) { + conn := testConn(t, testDB(t)) + username := randString(10) + + items := make([]Activity, 3) + for i := range items { + items[i] = genActivity(username) + if err := CreateActivity(conn, &items[i]); err != nil { + t.Fatalf("CreateActivity %d: %v", i, err) + } + } + other := genActivity(randString(10)) + if err := CreateActivity(conn, &other); err != nil { + t.Fatalf("CreateActivity other: %v", err) + } + + got, err := GetActivitiesForUsername(conn, username) + if err != nil { + t.Fatalf("GetActivitiesForUsername: %v", err) + } + if len(got) != 3 { + t.Errorf("got %d activities, want 3", len(got)) + } + byUID := make(map[string]Activity) + for _, a := range got { + byUID[a.UID] = a + } + for _, want := range items { + a, ok := byUID[want.UID] + if !ok { + t.Errorf("activity %q missing from results", want.UID) + continue + } + if a != want { + t.Errorf("activity %q: got %+v, want %+v", want.UID, a, want) + } + } +} + +func TestGetActivitiesForUsernameEmpty(t *testing.T) { + conn := testConn(t, testDB(t)) + + got, err := GetActivitiesForUsername(conn, randString(10)) + if err != nil { + t.Fatalf("GetActivitiesForUsername: %v", err) + } + if len(got) != 0 { + t.Errorf("expected empty slice, got %d items", len(got)) + } +}