add: tests using an actual database
This commit is contained in:
@@ -10,6 +10,9 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.34.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
|
||||||
golang.org/x/net v0.38.0 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/fergusstrange/embedded-postgres v1.34.0 h1:c6RKhPKFsLVU+Tdxsx8q0UxCHsvZZ/iShAnljRBXs6s=
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.34.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
@@ -8,6 +10,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed schema.sql
|
||||||
|
var Bytes []byte
|
||||||
@@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS stories (
|
|||||||
|
|
||||||
-- Constraints
|
-- Constraints
|
||||||
CONSTRAINT stories_story_id_unique UNIQUE (story_id),
|
CONSTRAINT stories_story_id_unique UNIQUE (story_id),
|
||||||
CONSTRAINT stories_story_id_pattern CHECK (story_id ~ '^[a-z][0-9-]{2,61}[0-9]$'),
|
CONSTRAINT stories_story_id_pattern CHECK (story_id ~ '^[a-z][a-z0-9-]{2,61}$'),
|
||||||
CONSTRAINT stories_etag_pattern CHECK (char_length(etag) > 0)
|
CONSTRAINT stories_etag_pattern CHECK (char_length(etag) > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
|
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/lib/pq"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,7 +244,7 @@ func (s *DBStory) insert(db *sql.DB) error {
|
|||||||
s.Title,
|
s.Title,
|
||||||
s.Content,
|
s.Content,
|
||||||
s.Description,
|
s.Description,
|
||||||
labels,
|
pq.Array(labels),
|
||||||
s.CreatedAt,
|
s.CreatedAt,
|
||||||
s.UpdatedAt,
|
s.UpdatedAt,
|
||||||
etag,
|
etag,
|
||||||
@@ -296,7 +297,7 @@ func (s *DBStory) update(db *sql.DB) error {
|
|||||||
s.Title,
|
s.Title,
|
||||||
s.Content,
|
s.Content,
|
||||||
s.Description,
|
s.Description,
|
||||||
labels,
|
pq.Array(labels),
|
||||||
s.UpdatedAt,
|
s.UpdatedAt,
|
||||||
etag,
|
etag,
|
||||||
s.ID,
|
s.ID,
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
package story
|
package story
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
|
v1 "git.tipsy.codes/charles/webstory/pkg/api/webstory/v1"
|
||||||
|
"git.tipsy.codes/charles/webstory/pkg/database/schema"
|
||||||
|
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
||||||
|
"github.com/lib/pq"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStoryTranslator_FromAPI(t *testing.T) {
|
func TestStoryTranslator_FromAPI(t *testing.T) {
|
||||||
trans := NewStoryTranslator()
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
apiStory *v1.Story
|
apiStory *v1.Story
|
||||||
@@ -189,6 +191,7 @@ func TestStoryTranslator_FromAPI(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
trans := NewStoryTranslator()
|
||||||
result, err := trans.FromAPI(tt.apiStory)
|
result, err := trans.FromAPI(tt.apiStory)
|
||||||
|
|
||||||
if tt.expectErr {
|
if tt.expectErr {
|
||||||
@@ -223,7 +226,6 @@ func TestStoryTranslator_FromAPI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStoryTranslator_ToAPI(t *testing.T) {
|
func TestStoryTranslator_ToAPI(t *testing.T) {
|
||||||
trans := NewStoryTranslator()
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -239,7 +241,7 @@ func TestStoryTranslator_ToAPI(t *testing.T) {
|
|||||||
expectErr: false,
|
expectErr: false,
|
||||||
checkFunc: func(s *v1.Story) error {
|
checkFunc: func(s *v1.Story) error {
|
||||||
if s != nil {
|
if s != nil {
|
||||||
return nil
|
return fmt.Errorf("expected nil, got %v", s)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -298,6 +300,7 @@ func TestStoryTranslator_ToAPI(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
trans := NewStoryTranslator()
|
||||||
result := trans.ToAPI(tt.dbStory)
|
result := trans.ToAPI(tt.dbStory)
|
||||||
|
|
||||||
if tt.expectErr {
|
if tt.expectErr {
|
||||||
@@ -380,3 +383,269 @@ func TestDBStory_Validate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDBStory_Save(t *testing.T) {
|
||||||
|
// Set up embedded postgres for testing
|
||||||
|
server := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().Port(5432))
|
||||||
|
err := server.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to start embedded postgres: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := server.Stop(); err != nil {
|
||||||
|
t.Errorf("failed to stop embedded postgres: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", "postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
_, err = db.ExecContext(ctx, string(schema.Bytes))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to run schema: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupFunc func() *DBStory
|
||||||
|
expectErr bool
|
||||||
|
checkFunc func(*testing.T, *sql.DB, *DBStory) error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "insert new story successfully",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
return &DBStory{
|
||||||
|
StoryID: "insert-test-1",
|
||||||
|
Title: "Insert Test Story",
|
||||||
|
Content: "Test content",
|
||||||
|
Labels: []string{"test", "insert"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
if story.ID == 0 {
|
||||||
|
return fmt.Errorf("expected non-zero ID after insert, got 0")
|
||||||
|
}
|
||||||
|
if story.CreatedAt.IsZero() {
|
||||||
|
return fmt.Errorf("expected non-zero CreatedAt")
|
||||||
|
}
|
||||||
|
if story.UpdatedAt.IsZero() {
|
||||||
|
return fmt.Errorf("expected non-zero UpdatedAt")
|
||||||
|
}
|
||||||
|
if story.Etag == "" {
|
||||||
|
return fmt.Errorf("expected non-empty etag")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify in database
|
||||||
|
var dbStory DBStory
|
||||||
|
var labels pq.StringArray
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT id, story_id, title, content, labels, created_at, updated_at, etag
|
||||||
|
FROM stories WHERE id = $1`,
|
||||||
|
story.ID).Scan(
|
||||||
|
&dbStory.ID, &dbStory.StoryID, &dbStory.Title, &dbStory.Content,
|
||||||
|
&labels, &dbStory.CreatedAt, &dbStory.UpdatedAt, &dbStory.Etag,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query inserted story: %w", err)
|
||||||
|
}
|
||||||
|
dbStory.Labels = labels
|
||||||
|
if dbStory.StoryID != story.StoryID {
|
||||||
|
return fmt.Errorf("story_id mismatch: expected %s, got %s", story.StoryID, dbStory.StoryID)
|
||||||
|
}
|
||||||
|
if dbStory.Title != story.Title {
|
||||||
|
return fmt.Errorf("title mismatch: expected %s, got %s", story.Title, dbStory.Title)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "insert new story with etag provided",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
return &DBStory{
|
||||||
|
StoryID: "insert-test-2",
|
||||||
|
Title: "Insert Test with Etag",
|
||||||
|
Etag: "custom-etag-123",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
// Verify etag was set (even if custom provided, DB may override)
|
||||||
|
var dbStory DBStory
|
||||||
|
err := db.QueryRow(`SELECT etag FROM stories WHERE story_id = $1`, "insert-test-2").Scan(&dbStory.Etag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query story: %w", err)
|
||||||
|
}
|
||||||
|
if dbStory.Etag == "" {
|
||||||
|
return fmt.Errorf("expected non-empty etag in database")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "insert new story with all fields",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
return &DBStory{
|
||||||
|
StoryID: "insert-test-3",
|
||||||
|
Title: "Full Story",
|
||||||
|
Content: "Full content here",
|
||||||
|
Description: "Full description",
|
||||||
|
Labels: []string{"tag1", "tag2", "tag3"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
var dbStory DBStory
|
||||||
|
var labels pq.StringArray
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT id, story_id, title, content, description, labels, created_at, updated_at, etag
|
||||||
|
FROM stories WHERE story_id = $1`,
|
||||||
|
"insert-test-3").Scan(
|
||||||
|
&dbStory.ID, &dbStory.StoryID, &dbStory.Title, &dbStory.Content,
|
||||||
|
&dbStory.Description, &labels, &dbStory.CreatedAt,
|
||||||
|
&dbStory.UpdatedAt, &dbStory.Etag,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query inserted story: %w", err)
|
||||||
|
}
|
||||||
|
dbStory.Labels = []string(labels)
|
||||||
|
if dbStory.Description != "Full description" {
|
||||||
|
return fmt.Errorf("description mismatch")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update existing story successfully",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
// First create the story
|
||||||
|
newStory := &DBStory{
|
||||||
|
StoryID: "update-test-1",
|
||||||
|
Title: "Original Title",
|
||||||
|
Content: "Original content",
|
||||||
|
}
|
||||||
|
if err := newStory.Save(db); err != nil {
|
||||||
|
t.Fatalf("failed to insert story for update test: %v", err)
|
||||||
|
}
|
||||||
|
// Now update it
|
||||||
|
newStory.ID = newStory.ID
|
||||||
|
newStory.Title = "Updated Title"
|
||||||
|
newStory.Content = "Updated content"
|
||||||
|
return newStory
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
if story.Title != "Updated Title" {
|
||||||
|
return fmt.Errorf("expected title to be 'Updated Title', got %s", story.Title)
|
||||||
|
}
|
||||||
|
if story.Content != "Updated content" {
|
||||||
|
return fmt.Errorf("expected content to be 'Updated content', got %s", story.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify in database
|
||||||
|
var dbStory DBStory
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT title, content FROM stories WHERE id = $1`,
|
||||||
|
story.ID).Scan(&dbStory.Title, &dbStory.Content)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query updated story: %w", err)
|
||||||
|
}
|
||||||
|
if dbStory.Title != "Updated Title" {
|
||||||
|
return fmt.Errorf("title mismatch in DB: expected 'Updated Title', got %s", dbStory.Title)
|
||||||
|
}
|
||||||
|
if dbStory.Content != "Updated content" {
|
||||||
|
return fmt.Errorf("content mismatch in DB")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update with labels changes",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
// First create
|
||||||
|
newStory := &DBStory{
|
||||||
|
StoryID: "update-test-2",
|
||||||
|
Title: "Label Update Test",
|
||||||
|
Labels: []string{"old-tag"},
|
||||||
|
}
|
||||||
|
if err := newStory.Save(db); err != nil {
|
||||||
|
t.Fatalf("failed to insert story for label update test: %v", err)
|
||||||
|
}
|
||||||
|
// Update
|
||||||
|
newStory.ID = newStory.ID
|
||||||
|
newStory.Labels = []string{"new-tag-1", "new-tag-2"}
|
||||||
|
return newStory
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
var dbLabels pq.StringArray
|
||||||
|
err := db.QueryRow(`SELECT labels FROM stories WHERE id = $1`, story.ID).Scan(&dbLabels)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query labels: %w", err)
|
||||||
|
}
|
||||||
|
if len(dbLabels) != 2 {
|
||||||
|
return fmt.Errorf("expected 2 labels, got %d", len(dbLabels))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "insert with invalid story_id fails",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
return &DBStory{
|
||||||
|
StoryID: "INVALID",
|
||||||
|
Title: "Should Fail",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "insert with empty title fails",
|
||||||
|
setupFunc: func() *DBStory {
|
||||||
|
return &DBStory{
|
||||||
|
StoryID: "empty-title-test",
|
||||||
|
Title: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
checkFunc: func(t *testing.T, db *sql.DB, story *DBStory) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
story := tt.setupFunc()
|
||||||
|
|
||||||
|
err := story.Save(db)
|
||||||
|
|
||||||
|
if tt.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error but got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.checkFunc != nil {
|
||||||
|
if err := tt.checkFunc(t, db, story); err != nil {
|
||||||
|
t.Errorf("check failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user