Compare commits
1 Commits
0ec233e7c5
...
main
Author | SHA1 | Date | |
---|---|---|---|
07968a3a46 |
40
.gitea/workflows/validate.yaml
Normal file
40
.gitea/workflows/validate.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Validate the build
|
||||||
|
run-name: ${{ gitea.actor }} is validating
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:current-alpine
|
||||||
|
steps:
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
echo "https://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories
|
||||||
|
echo "https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories
|
||||||
|
apk update
|
||||||
|
apk add --no-cache git make bash go
|
||||||
|
|
||||||
|
GOBIN=/usr/local/bin go install mvdan.cc/gofumpt@latest
|
||||||
|
|
||||||
|
export "PATH=$PATH:/root/go/bin"
|
||||||
|
|
||||||
|
echo "---------------------"
|
||||||
|
echo "Go version:"
|
||||||
|
go version
|
||||||
|
echo "---------------------"
|
||||||
|
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Fetch dependencies
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
- name: Validate the code and formatting
|
||||||
|
run: |
|
||||||
|
make validate
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
make test
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
/bin
|
/bin
|
||||||
/reports
|
|
||||||
|
7
Makefile
7
Makefile
@@ -10,7 +10,6 @@ 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
|
||||||
@@ -26,10 +25,7 @@ $(BINARIES): %: $(BINARY_DIR)
|
|||||||
go build -ldflags '$(LDFLAGS)' -o $(BINARY_DIR)/$@ ./cmd/$@/
|
go build -ldflags '$(LDFLAGS)' -o $(BINARY_DIR)/$@ ./cmd/$@/
|
||||||
|
|
||||||
test:
|
test:
|
||||||
mkdir -p reports/coverage/
|
go test ./...
|
||||||
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)
|
||||||
@@ -37,7 +33,6 @@ validate:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BINARY_DIR)
|
rm -rf $(BINARY_DIR)
|
||||||
rm -rf reports
|
|
||||||
go clean
|
go clean
|
||||||
|
|
||||||
run: $(LINKSERV)
|
run: $(LINKSERV)
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -134,12 +135,8 @@ func (db *DB) CheckSchemaVersion() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transaction executes the provided function within a SQL transaction.
|
func (db *DB) transaction(ctx context.Context, fn func(*sql.Tx) error) error {
|
||||||
// If the function returns an error, the transaction is rolled back.
|
tx, err := db.conn.BeginTx(ctx, nil)
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
@@ -1,314 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
Reference in New Issue
Block a user