Compare commits
	
		
			6 Commits
		
	
	
		
			f7c72626ee
			...
			api_start
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 50e1e3345b | |||
| 914836cf34 | |||
| 0ec233e7c5 | |||
| 925d588f71 | |||
| eac3bc4ff5 | |||
| a9a9b4d9bb | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1 +1,2 @@
 | 
				
			|||||||
/bin
 | 
					/bin
 | 
				
			||||||
 | 
					/reports
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										7
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
								
							@@ -10,6 +10,7 @@ COMMIT_DATETIME := $(shell git log -1 --format=%cd --date=iso8601)
 | 
				
			|||||||
LDFLAGS := -X git.omicron.one/omicron/linkshare/internal/version.Version=$(VERSION) \
 | 
					LDFLAGS := -X git.omicron.one/omicron/linkshare/internal/version.Version=$(VERSION) \
 | 
				
			||||||
           -X git.omicron.one/omicron/linkshare/internal/version.GitCommit=$(COMMIT) \
 | 
					           -X git.omicron.one/omicron/linkshare/internal/version.GitCommit=$(COMMIT) \
 | 
				
			||||||
           -X "git.omicron.one/omicron/linkshare/internal/version.CommitDateTime=$(COMMIT_DATETIME)"
 | 
					           -X "git.omicron.one/omicron/linkshare/internal/version.CommitDateTime=$(COMMIT_DATETIME)"
 | 
				
			||||||
 | 
					OPEN = xdg-open
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
all: build
 | 
					all: build
 | 
				
			||||||
@@ -25,7 +26,10 @@ $(BINARIES): %: $(BINARY_DIR)
 | 
				
			|||||||
	go build -ldflags '$(LDFLAGS)' -o $(BINARY_DIR)/$@ ./cmd/$@/
 | 
						go build -ldflags '$(LDFLAGS)' -o $(BINARY_DIR)/$@ ./cmd/$@/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test:
 | 
					test:
 | 
				
			||||||
	go test ./...
 | 
						mkdir -p reports/coverage/
 | 
				
			||||||
 | 
						go test ./... -coverprofile=reports/coverage/coverage.out
 | 
				
			||||||
 | 
						go tool cover -html=reports/coverage/coverage.out -o reports/coverage/coverage.html && $(OPEN) reports/coverage/coverage.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
validate:
 | 
					validate:
 | 
				
			||||||
	@test -z "$(shell gofumpt -l .)" && echo "No files need formatting" || (echo "Incorrect formatting in:"; gofumpt  -l .; exit 1)
 | 
						@test -z "$(shell gofumpt -l .)" && echo "No files need formatting" || (echo "Incorrect formatting in:"; gofumpt  -l .; exit 1)
 | 
				
			||||||
@@ -33,6 +37,7 @@ validate:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
clean:
 | 
					clean:
 | 
				
			||||||
	rm -rf $(BINARY_DIR)
 | 
						rm -rf $(BINARY_DIR)
 | 
				
			||||||
 | 
						rm -rf reports
 | 
				
			||||||
	go clean
 | 
						go clean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
run: $(LINKSERV)
 | 
					run: $(LINKSERV)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										192
									
								
								internal/database/links/links.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								internal/database/links/links.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,192 @@
 | 
				
			|||||||
 | 
					package links
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.omicron.one/omicron/linkshare/internal/database"
 | 
				
			||||||
 | 
						. "git.omicron.one/omicron/linkshare/internal/util/option"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Link represents a stored link
 | 
				
			||||||
 | 
					type Link struct {
 | 
				
			||||||
 | 
						ID        int64
 | 
				
			||||||
 | 
						URL       string
 | 
				
			||||||
 | 
						Title     string
 | 
				
			||||||
 | 
						CreatedAt time.Time
 | 
				
			||||||
 | 
						UpdatedAt Option[time.Time]
 | 
				
			||||||
 | 
						IsPrivate bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Repository handles link storage operations
 | 
				
			||||||
 | 
					type Repository struct {
 | 
				
			||||||
 | 
						db *database.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewRepository creates a new link repository
 | 
				
			||||||
 | 
					func NewRepository(db *database.DB) *Repository {
 | 
				
			||||||
 | 
						return &Repository{db: db}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Create adds a new link to the database
 | 
				
			||||||
 | 
					func (r *Repository) Create(url, title string, isPrivate bool) (int64, error) {
 | 
				
			||||||
 | 
						var id int64
 | 
				
			||||||
 | 
						err := r.db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							now := time.Now().UTC().Format(time.RFC3339)
 | 
				
			||||||
 | 
							result, err := tx.Exec(
 | 
				
			||||||
 | 
								"INSERT INTO links (url, title, created_at, is_private) VALUES (?, ?, ?, ?)",
 | 
				
			||||||
 | 
								url, title, now, isPrivate,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							id, err = result.LastInsertId()
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return id, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Get retrieves a single link by ID
 | 
				
			||||||
 | 
					func (r *Repository) Get(id int64) (*Link, error) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							link      Link
 | 
				
			||||||
 | 
							createdAt string
 | 
				
			||||||
 | 
							updatedAt sql.NullString
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := r.db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							row := tx.QueryRow(
 | 
				
			||||||
 | 
								"SELECT id, url, title, created_at, updated_at, is_private FROM links WHERE id = ?",
 | 
				
			||||||
 | 
								id,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err := row.Scan(&link.ID, &link.URL, &link.Title, &createdAt, &updatedAt, &link.IsPrivate)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							created, err := time.Parse(time.RFC3339, createdAt)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							link.CreatedAt = created
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if updatedAt.Valid {
 | 
				
			||||||
 | 
								updated, err := time.Parse(time.RFC3339, updatedAt.String)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								link.UpdatedAt = Some(updated)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								link.UpdatedAt = None[time.Time]()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &link, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update updates an existing link's fields
 | 
				
			||||||
 | 
					func (r *Repository) Update(id int64, url, title string, isPrivate bool) error {
 | 
				
			||||||
 | 
						return r.db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							now := time.Now().UTC().Format(time.RFC3339)
 | 
				
			||||||
 | 
							_, err := tx.Exec(
 | 
				
			||||||
 | 
								"UPDATE links SET url = ?, title = ?, updated_at = ?, is_private = ? WHERE id = ?",
 | 
				
			||||||
 | 
								url, title, now, isPrivate, id,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Delete removes a link from the database
 | 
				
			||||||
 | 
					func (r *Repository) Delete(id int64) error {
 | 
				
			||||||
 | 
						return r.db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							_, err := tx.Exec("DELETE FROM links WHERE id = ?", id)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// List returns a paginated list of links
 | 
				
			||||||
 | 
					func (r *Repository) List(includePrivate bool, offset, limit int) ([]*Link, error) {
 | 
				
			||||||
 | 
						var links []*Link
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := r.db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							var rows *sql.Rows
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if includePrivate {
 | 
				
			||||||
 | 
								rows, err = tx.Query(
 | 
				
			||||||
 | 
									`SELECT id, url, title, created_at, updated_at, is_private
 | 
				
			||||||
 | 
									FROM links ORDER BY created_at DESC LIMIT ? OFFSET ?`,
 | 
				
			||||||
 | 
									limit, offset,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								rows, err = tx.Query(
 | 
				
			||||||
 | 
									`SELECT id, url, title, created_at, updated_at, is_private
 | 
				
			||||||
 | 
									FROM links WHERE is_private = 0 ORDER BY created_at DESC LIMIT ? OFFSET ?`,
 | 
				
			||||||
 | 
									limit, offset,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer rows.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for rows.Next() {
 | 
				
			||||||
 | 
								var (
 | 
				
			||||||
 | 
									link      Link
 | 
				
			||||||
 | 
									createdAt string
 | 
				
			||||||
 | 
									updatedAt sql.NullString
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err := rows.Scan(&link.ID, &link.URL, &link.Title, &createdAt, &updatedAt, &link.IsPrivate)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								created, err := time.Parse(time.RFC3339, createdAt)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								link.CreatedAt = created
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if updatedAt.Valid {
 | 
				
			||||||
 | 
									updated, err := time.Parse(time.RFC3339, updatedAt.String)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									link.UpdatedAt = Some(updated)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									link.UpdatedAt = None[time.Time]()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								links = append(links, &link)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return rows.Err()
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return links, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Count returns the total number of links in the database
 | 
				
			||||||
 | 
					func (r *Repository) Count(includePrivate bool) (int, error) {
 | 
				
			||||||
 | 
						var count int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := r.db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							var row *sql.Row
 | 
				
			||||||
 | 
							if includePrivate {
 | 
				
			||||||
 | 
								row = tx.QueryRow("SELECT COUNT(*) FROM links")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								row = tx.QueryRow("SELECT COUNT(*) FROM links WHERE is_private")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return row.Scan(&count)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return count, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										329
									
								
								internal/database/links/links_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								internal/database/links/links_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,329 @@
 | 
				
			|||||||
 | 
					package links_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.omicron.one/omicron/linkshare/internal/database"
 | 
				
			||||||
 | 
						"git.omicron.one/omicron/linkshare/internal/database/links"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setupTestDB(t *testing.T) (*database.DB, string) {
 | 
				
			||||||
 | 
						t.Helper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cwd, err := os.Getwd()
 | 
				
			||||||
 | 
						t.Logf("Current working directory: %s", cwd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create temp file for database
 | 
				
			||||||
 | 
						tempFile, err := os.CreateTemp("", "linkshare-links-test-*.db")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tempFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dbPath := tempFile.Name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open database
 | 
				
			||||||
 | 
						db, err := database.Open(dbPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize database with schema
 | 
				
			||||||
 | 
						err = db.Initialize("../../../schema")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
							t.Fatalf("Failed to initialize database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return db, dbPath
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_Create(t *testing.T) {
 | 
				
			||||||
 | 
						db, dbPath := setupTestDB(t)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := links.NewRepository(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test creating a link
 | 
				
			||||||
 | 
						id, err := repo.Create("https://example.com", "Example", false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if id <= 0 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected positive ID, got %d", id)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify link was created by retrieving it
 | 
				
			||||||
 | 
						link, err := repo.Get(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if link.URL != "https://example.com" {
 | 
				
			||||||
 | 
							t.Errorf("Expected URL 'https://example.com', got '%s'", link.URL)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_Get(t *testing.T) {
 | 
				
			||||||
 | 
						db, dbPath := setupTestDB(t)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := links.NewRepository(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert test data
 | 
				
			||||||
 | 
						id, err := repo.Create("https://example.com", "Example", true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting a link
 | 
				
			||||||
 | 
						link, err := repo.Get(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if link.ID != id {
 | 
				
			||||||
 | 
							t.Errorf("Expected ID %d, got %d", id, link.ID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link.URL != "https://example.com" {
 | 
				
			||||||
 | 
							t.Errorf("Expected URL 'https://example.com', got '%s'", link.URL)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link.Title != "Example" {
 | 
				
			||||||
 | 
							t.Errorf("Expected Title 'Example', got '%s'", link.Title)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link.IsPrivate != true {
 | 
				
			||||||
 | 
							t.Errorf("Expected IsPrivate true, got %v", link.IsPrivate)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link.UpdatedAt.IsSome() {
 | 
				
			||||||
 | 
							t.Errorf("Expected UpdatedAt to be None, got %v", link.UpdatedAt)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting non-existent link
 | 
				
			||||||
 | 
						_, err = repo.Get(id + 1)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Fatal("Expected error when getting non-existent link")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_Update(t *testing.T) {
 | 
				
			||||||
 | 
						db, dbPath := setupTestDB(t)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := links.NewRepository(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert test data
 | 
				
			||||||
 | 
						id, err := repo.Create("https://example.com", "Example", false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test updating a link
 | 
				
			||||||
 | 
						err = repo.Update(id, "https://updated.com", "Updated", true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to update link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify link was updated
 | 
				
			||||||
 | 
						link, err := repo.Get(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if link.URL != "https://updated.com" {
 | 
				
			||||||
 | 
							t.Errorf("Expected URL 'https://updated.com', got '%s'", link.URL)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link.Title != "Updated" {
 | 
				
			||||||
 | 
							t.Errorf("Expected Title 'Updated', got '%s'", link.Title)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link.IsPrivate != true {
 | 
				
			||||||
 | 
							t.Errorf("Expected IsPrivate true, got %v", link.IsPrivate)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !link.UpdatedAt.IsSome() {
 | 
				
			||||||
 | 
							t.Error("Expected UpdatedAt to be set")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_Delete(t *testing.T) {
 | 
				
			||||||
 | 
						db, dbPath := setupTestDB(t)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := links.NewRepository(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert test data
 | 
				
			||||||
 | 
						id, err := repo.Create("https://example.com", "Example", false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test deleting a link
 | 
				
			||||||
 | 
						err = repo.Delete(id)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to delete link: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify link was deleted
 | 
				
			||||||
 | 
						_, err = repo.Get(id)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Fatal("Expected error after deletion")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_List(t *testing.T) {
 | 
				
			||||||
 | 
						db, dbPath := setupTestDB(t)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := links.NewRepository(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert test data
 | 
				
			||||||
 | 
						urls := []struct {
 | 
				
			||||||
 | 
							url       string
 | 
				
			||||||
 | 
							isPrivate bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{"https://example1.com", true},
 | 
				
			||||||
 | 
							{"https://example2.com", false},
 | 
				
			||||||
 | 
							{"https://example3.com", false},
 | 
				
			||||||
 | 
							{"https://example4.com", true},
 | 
				
			||||||
 | 
							{"https://example5.com", false},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, info := range urls {
 | 
				
			||||||
 | 
							_, err := repo.Create(info.url, "Example "+string(rune('A'+i)), info.isPrivate)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create link: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Add a small delay to ensure different created_at times
 | 
				
			||||||
 | 
							time.Sleep(10 * time.Millisecond)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test full listing with pagination
 | 
				
			||||||
 | 
						links, err := repo.List(true, 0, 3)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(links) != 3 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 3 links, got %d", len(links))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check order (newest first)
 | 
				
			||||||
 | 
						for i := 0; i < len(links)-1; i++ {
 | 
				
			||||||
 | 
							if links[i].CreatedAt.Before(links[i+1].CreatedAt) {
 | 
				
			||||||
 | 
								t.Errorf("Links not in correct order")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test second page of full listing
 | 
				
			||||||
 | 
						links, err = repo.List(true, 3, 2)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(links) != 2 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 2 links, got %d", len(links))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test public listing
 | 
				
			||||||
 | 
						links, err = repo.List(false, 0, 3)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(links) != 3 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 3 links, got %d", len(links))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, link := range links {
 | 
				
			||||||
 | 
							if link.IsPrivate {
 | 
				
			||||||
 | 
								t.Fatalf("private link in public listing %v", link)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to get more public links
 | 
				
			||||||
 | 
						links, err = repo.List(false, 3, 3)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(links) != 0 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 0 links, got %d", len(links))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRepository_Count(t *testing.T) {
 | 
				
			||||||
 | 
						db, dbPath := setupTestDB(t)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							db.Close()
 | 
				
			||||||
 | 
							os.Remove(dbPath)
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repo := links.NewRepository(db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check full count with empty table
 | 
				
			||||||
 | 
						count, err := repo.Count(true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to count links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count != 0 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 0 links, got %d", count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check public count with empty table
 | 
				
			||||||
 | 
						count, err = repo.Count(false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to count links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count != 0 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 0 links, got %d", count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert test data
 | 
				
			||||||
 | 
						numLinks := 5
 | 
				
			||||||
 | 
						for i := 0; i < numLinks; i++ {
 | 
				
			||||||
 | 
							_, err := repo.Create(
 | 
				
			||||||
 | 
								"https://example"+string(rune('1'+i))+".com",
 | 
				
			||||||
 | 
								"Example "+string(rune('A'+i)),
 | 
				
			||||||
 | 
								i%2 == 1,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create link: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pubLinks := numLinks / 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check full count again
 | 
				
			||||||
 | 
						count, err = repo.Count(true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to count links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count != numLinks {
 | 
				
			||||||
 | 
							t.Fatalf("Expected %d links, got %d", numLinks, count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check public count again
 | 
				
			||||||
 | 
						count, err = repo.Count(false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to count links: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count != pubLinks {
 | 
				
			||||||
 | 
							t.Fatalf("Expected %d links, got %d", pubLinks, count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,7 +5,6 @@
 | 
				
			|||||||
package database
 | 
					package database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"database/sql"
 | 
						"database/sql"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
@@ -135,8 +134,12 @@ func (db *DB) CheckSchemaVersion() error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (db *DB) transaction(ctx context.Context, fn func(*sql.Tx) error) error {
 | 
					// Transaction executes the provided function within a SQL transaction.
 | 
				
			||||||
	tx, err := db.conn.BeginTx(ctx, nil)
 | 
					// If the function returns an error, the transaction is rolled back.
 | 
				
			||||||
 | 
					// If the function panics, the transaction is rolled back and the panic is re-thrown.
 | 
				
			||||||
 | 
					// The function receives a *sql.Tx that can be used for database operations.
 | 
				
			||||||
 | 
					func (db *DB) Transaction(fn func(*sql.Tx) error) error {
 | 
				
			||||||
 | 
						tx, err := db.conn.Begin()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to begin transaction: %w", err)
 | 
							return fmt.Errorf("failed to begin transaction: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										314
									
								
								internal/database/manager_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								internal/database/manager_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,314 @@
 | 
				
			|||||||
 | 
					package database
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.omicron.one/omicron/linkshare/internal/version"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestOpenClose(t *testing.T) {
 | 
				
			||||||
 | 
						// Create temp file for database
 | 
				
			||||||
 | 
						tempFile, err := os.CreateTemp("", "linkshare-test-*.db")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.Remove(tempFile.Name())
 | 
				
			||||||
 | 
						tempFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test opening
 | 
				
			||||||
 | 
						db, err := Open(tempFile.Name())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test closing
 | 
				
			||||||
 | 
						err = db.Close()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to close database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestInitialize(t *testing.T) {
 | 
				
			||||||
 | 
						// Create temp directory for test data
 | 
				
			||||||
 | 
						tempDir, err := os.MkdirTemp("", "linkshare-test-*")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.RemoveAll(tempDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create schema directory and current.sql file
 | 
				
			||||||
 | 
						schemaDir := filepath.Join(tempDir, "schema")
 | 
				
			||||||
 | 
						err = os.Mkdir(schemaDir, 0o755)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create schema directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write test schema to file
 | 
				
			||||||
 | 
						schemaContent := `CREATE TABLE settings (
 | 
				
			||||||
 | 
					    key TEXT PRIMARY KEY,
 | 
				
			||||||
 | 
					    value TEXT NOT NULL,
 | 
				
			||||||
 | 
					    kind TEXT CHECK(kind IN ('int', 'string', 'bool', 'json', 'glob')) NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					INSERT INTO settings (key, value, kind) VALUES ('schema-version', '1', 'int');`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = os.WriteFile(filepath.Join(schemaDir, "current.sql"), []byte(schemaContent), 0o644)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to write schema file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create temp database file
 | 
				
			||||||
 | 
						dbPath := filepath.Join(tempDir, "test.db")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open database
 | 
				
			||||||
 | 
						db, err := Open(dbPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test initialization
 | 
				
			||||||
 | 
						err = db.Initialize(schemaDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to initialize database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test already initialized error
 | 
				
			||||||
 | 
						err = db.Initialize(schemaDir)
 | 
				
			||||||
 | 
						if err != ErrAlreadyInitialized {
 | 
				
			||||||
 | 
							t.Fatalf("Expected ErrAlreadyInitialized, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCheckInitialized(t *testing.T) {
 | 
				
			||||||
 | 
						// Create temp file for database
 | 
				
			||||||
 | 
						tempFile, err := os.CreateTemp("", "linkshare-test-*.db")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.Remove(tempFile.Name())
 | 
				
			||||||
 | 
						tempFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open database
 | 
				
			||||||
 | 
						db, err := Open(tempFile.Name())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test not initialized
 | 
				
			||||||
 | 
						err = db.CheckInitialized()
 | 
				
			||||||
 | 
						if err != ErrNotInitialized {
 | 
				
			||||||
 | 
							t.Fatalf("Expected ErrNotInitialized, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize the database manually for testing
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL, kind TEXT NOT NULL)")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create settings table: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test initialized
 | 
				
			||||||
 | 
						err = db.CheckInitialized()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Expected nil error after initialization, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetSchemaVersion(t *testing.T) {
 | 
				
			||||||
 | 
						// Create temp file for database
 | 
				
			||||||
 | 
						tempFile, err := os.CreateTemp("", "linkshare-test-*.db")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.Remove(tempFile.Name())
 | 
				
			||||||
 | 
						tempFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open database
 | 
				
			||||||
 | 
						db, err := Open(tempFile.Name())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize the database manually for testing
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL, kind TEXT NOT NULL)")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create settings table: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("INSERT INTO settings (key, value, kind) VALUES ('schema-version', '1', 'int')")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to insert schema version: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test schema version
 | 
				
			||||||
 | 
						version, err := db.GetSchemaVersion()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get schema version: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if version != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected schema version 1, got: %d", version)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test invalid schema version
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("UPDATE settings SET value = 'invalid' WHERE key = 'schema-version'")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to update schema version: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err = db.GetSchemaVersion()
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Fatal("Expected error for invalid schema version, got nil")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCheckSchemaVersion(t *testing.T) {
 | 
				
			||||||
 | 
						// Create temp file for database
 | 
				
			||||||
 | 
						tempFile, err := os.CreateTemp("", "linkshare-test-*.db")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.Remove(tempFile.Name())
 | 
				
			||||||
 | 
						tempFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open database
 | 
				
			||||||
 | 
						db, err := Open(tempFile.Name())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test not initialized
 | 
				
			||||||
 | 
						err = db.CheckSchemaVersion()
 | 
				
			||||||
 | 
						if err != ErrNotInitialized {
 | 
				
			||||||
 | 
							t.Fatalf("Expected ErrNotInitialized, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize the database manually
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL, kind TEXT NOT NULL)")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create settings table: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Store current schema version
 | 
				
			||||||
 | 
						originalSchemaVersion := version.SchemaVersion
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							// Restore original schema version after test
 | 
				
			||||||
 | 
							version.SchemaVersion = originalSchemaVersion
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test version match
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("INSERT INTO settings (key, value, kind) VALUES ('schema-version', '1', 'int')")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to insert schema version: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						version.SchemaVersion = 1
 | 
				
			||||||
 | 
						err = db.CheckSchemaVersion()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Expected nil error for matching schema versions, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test outdated version
 | 
				
			||||||
 | 
						version.SchemaVersion = 2
 | 
				
			||||||
 | 
						err = db.CheckSchemaVersion()
 | 
				
			||||||
 | 
						if err != ErrSchemaOutdated {
 | 
				
			||||||
 | 
							t.Fatalf("Expected ErrSchemaOutdated, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test unsupported version
 | 
				
			||||||
 | 
						version.SchemaVersion = 1
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("UPDATE settings SET value = '2' WHERE key = 'schema-version'")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to update schema version: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = db.CheckSchemaVersion()
 | 
				
			||||||
 | 
						if err != ErrSchemaUnsupported {
 | 
				
			||||||
 | 
							t.Fatalf("Expected ErrSchemaUnsupported, got: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestTransaction(t *testing.T) {
 | 
				
			||||||
 | 
						// Create temp file for database
 | 
				
			||||||
 | 
						tempFile, err := os.CreateTemp("", "linkshare-test-*.db")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.Remove(tempFile.Name())
 | 
				
			||||||
 | 
						tempFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Open database
 | 
				
			||||||
 | 
						db, err := Open(tempFile.Name())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to open database: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initialize the database manually for testing
 | 
				
			||||||
 | 
						_, err = db.conn.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create test table: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test successful transaction
 | 
				
			||||||
 | 
						err = db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							_, err := tx.Exec("INSERT INTO test (value) VALUES (?)", "test-value")
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Transaction failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify data was inserted
 | 
				
			||||||
 | 
						var value string
 | 
				
			||||||
 | 
						err = db.conn.QueryRow("SELECT value FROM test WHERE id = 1").Scan(&value)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to query test value: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if value != "test-value" {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 'test-value', got: %s", value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test failed transaction
 | 
				
			||||||
 | 
						err = db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
							_, err := tx.Exec("INSERT INTO test (value) VALUES (?)", "should-rollback")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return sql.ErrTxDone // Force rollback
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Fatal("Expected error from failed transaction, got nil")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Verify data was not inserted (rollback worked)
 | 
				
			||||||
 | 
						var count int
 | 
				
			||||||
 | 
						err = db.conn.QueryRow("SELECT COUNT(*) FROM test WHERE value = 'should-rollback'").Scan(&count)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to query test count: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count != 0 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected count 0 after rollback, got: %d", count)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test panic in transaction
 | 
				
			||||||
 | 
						panicked := false
 | 
				
			||||||
 | 
						func() {
 | 
				
			||||||
 | 
							defer func() {
 | 
				
			||||||
 | 
								if r := recover(); r != nil {
 | 
				
			||||||
 | 
									panicked = true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_ = db.Transaction(func(tx *sql.Tx) error {
 | 
				
			||||||
 | 
								panic("test panic")
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !panicked {
 | 
				
			||||||
 | 
							t.Fatal("Expected panic to be propagated")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								internal/util/option/option.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								internal/util/option/option.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					package option
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Option[T any] struct {
 | 
				
			||||||
 | 
						hasValue bool
 | 
				
			||||||
 | 
						value    T
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Some[T any](value T) Option[T] {
 | 
				
			||||||
 | 
						return Option[T]{
 | 
				
			||||||
 | 
							hasValue: true,
 | 
				
			||||||
 | 
							value:    value,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func None[T any]() Option[T] {
 | 
				
			||||||
 | 
						return Option[T]{
 | 
				
			||||||
 | 
							hasValue: false,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o Option[T]) IsSome() bool {
 | 
				
			||||||
 | 
						return o.hasValue
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o Option[T]) IsNone() bool {
 | 
				
			||||||
 | 
						return !o.hasValue
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o Option[T]) Value() T {
 | 
				
			||||||
 | 
						if !o.hasValue {
 | 
				
			||||||
 | 
							panic("Option has no value")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return o.value
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o Option[T]) ValueOr(defaultValue T) T {
 | 
				
			||||||
 | 
						if !o.hasValue {
 | 
				
			||||||
 | 
							return defaultValue
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return o.value
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								internal/util/option/option_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								internal/util/option/option_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					package option_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						. "git.omicron.one/omicron/linkshare/internal/util/option"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSome(t *testing.T) {
 | 
				
			||||||
 | 
						opt := Some(42)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !opt.IsSome() {
 | 
				
			||||||
 | 
							t.Error("Expected IsSome() to be true for Some(42)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opt.IsNone() {
 | 
				
			||||||
 | 
							t.Error("Expected IsNone() to be false for Some(42)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opt.Value() != 42 {
 | 
				
			||||||
 | 
							t.Errorf("Expected Value() to be 42, got %v", opt.Value())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opt.ValueOr(0) != 42 {
 | 
				
			||||||
 | 
							t.Errorf("Expected ValueOr(0) to be 42, got %v", opt.ValueOr(0))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestNone(t *testing.T) {
 | 
				
			||||||
 | 
						opt := None[int]()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opt.IsSome() {
 | 
				
			||||||
 | 
							t.Error("Expected IsSome() to be false for None[int]()")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !opt.IsNone() {
 | 
				
			||||||
 | 
							t.Error("Expected IsNone() to be true for None[int]()")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opt.ValueOr(99) != 99 {
 | 
				
			||||||
 | 
							t.Errorf("Expected ValueOr(99) to be 99, got %v", opt.ValueOr(99))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPanic(t *testing.T) {
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if r := recover(); r == nil {
 | 
				
			||||||
 | 
								t.Error("Expected Value() to panic on None")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opt := None[string]()
 | 
				
			||||||
 | 
						_ = opt.Value() // This should panic
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -16,4 +16,5 @@ CREATE TABLE links (
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE INDEX idx_links_created_at ON links(created_at);
 | 
					CREATE INDEX idx_links_created_at ON links(created_at);
 | 
				
			||||||
 | 
					CREATE INDEX idx_links_is_private_created_at ON links(is_private, created_at);
 | 
				
			||||||
CREATE INDEX idx_links_url ON links(url);
 | 
					CREATE INDEX idx_links_url ON links(url);
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user