307 lines
7.9 KiB
Go
307 lines
7.9 KiB
Go
package db_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"lmika.dev/lmika/weiro/models"
|
|
"lmika.dev/lmika/weiro/providers/db"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func newTestDB(t *testing.T) *db.Provider {
|
|
t.Helper()
|
|
dbFile := filepath.Join(t.TempDir(), "test.db")
|
|
p, err := db.New(dbFile)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { p.Close() })
|
|
return p
|
|
}
|
|
|
|
func TestProvider_Users(t *testing.T) {
|
|
ctx := context.Background()
|
|
p := newTestDB(t)
|
|
|
|
t.Run("save and select user", func(t *testing.T) {
|
|
user := &models.User{
|
|
Username: "alice",
|
|
PasswordHashed: []byte("hashed-password"),
|
|
}
|
|
|
|
err := p.SaveUser(ctx, user)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, user.ID)
|
|
|
|
got, err := p.SelectUserByUsername(ctx, "alice")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user.ID, got.ID)
|
|
assert.Equal(t, "alice", got.Username)
|
|
assert.Equal(t, []byte("hashed-password"), got.PasswordHashed)
|
|
})
|
|
|
|
t.Run("update user", func(t *testing.T) {
|
|
user := &models.User{
|
|
Username: "bob",
|
|
PasswordHashed: []byte("old-password"),
|
|
}
|
|
err := p.SaveUser(ctx, user)
|
|
require.NoError(t, err)
|
|
|
|
user.Username = "bob"
|
|
user.PasswordHashed = []byte("new-password")
|
|
err = p.SaveUser(ctx, user)
|
|
require.NoError(t, err)
|
|
|
|
got, err := p.SelectUserByUsername(ctx, "bob")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("new-password"), got.PasswordHashed)
|
|
})
|
|
}
|
|
|
|
func TestProvider_Sites(t *testing.T) {
|
|
ctx := context.Background()
|
|
p := newTestDB(t)
|
|
|
|
// Create a user first (sites need an owner)
|
|
user := &models.User{
|
|
Username: "testuser",
|
|
PasswordHashed: []byte("password"),
|
|
}
|
|
require.NoError(t, p.SaveUser(ctx, user))
|
|
|
|
t.Run("save and select sites", func(t *testing.T) {
|
|
site := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "My Blog",
|
|
Tagline: "A test blog",
|
|
}
|
|
|
|
err := p.SaveSite(ctx, site)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, site.ID)
|
|
|
|
sites, err := p.SelectSitesOwnedByUser(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, sites, 1)
|
|
assert.Equal(t, site.ID, sites[0].ID)
|
|
assert.Equal(t, user.ID, sites[0].OwnerID)
|
|
assert.Equal(t, "My Blog", sites[0].Title)
|
|
assert.Equal(t, "A test blog", sites[0].Tagline)
|
|
})
|
|
|
|
t.Run("select site by id", func(t *testing.T) {
|
|
site := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "Lookup Blog",
|
|
Tagline: "Find me by ID",
|
|
}
|
|
require.NoError(t, p.SaveSite(ctx, site))
|
|
|
|
got, err := p.SelectSiteByID(ctx, site.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, site.ID, got.ID)
|
|
assert.Equal(t, user.ID, got.OwnerID)
|
|
assert.Equal(t, "Lookup Blog", got.Title)
|
|
assert.Equal(t, "Find me by ID", got.Tagline)
|
|
})
|
|
|
|
t.Run("select sites for user with no sites", func(t *testing.T) {
|
|
otherUser := &models.User{
|
|
Username: "otheruser",
|
|
PasswordHashed: []byte("password"),
|
|
}
|
|
require.NoError(t, p.SaveUser(ctx, otherUser))
|
|
|
|
sites, err := p.SelectSitesOwnedByUser(ctx, otherUser.ID)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, sites)
|
|
})
|
|
}
|
|
|
|
func TestProvider_Posts(t *testing.T) {
|
|
ctx := context.Background()
|
|
p := newTestDB(t)
|
|
|
|
// Create user and site
|
|
user := &models.User{
|
|
Username: "testuser",
|
|
PasswordHashed: []byte("password"),
|
|
}
|
|
require.NoError(t, p.SaveUser(ctx, user))
|
|
|
|
site := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "My Blog",
|
|
Tagline: "A test blog",
|
|
}
|
|
require.NoError(t, p.SaveSite(ctx, site))
|
|
|
|
t.Run("save and select posts", func(t *testing.T) {
|
|
now := time.Date(2026, 2, 19, 12, 0, 0, 0, time.UTC)
|
|
post := &models.Post{
|
|
SiteID: site.ID,
|
|
GUID: "post-001",
|
|
Title: "First Post",
|
|
Body: "Hello world",
|
|
Slug: "/2026/02/19/first-post",
|
|
CreatedAt: now,
|
|
PublishedAt: now,
|
|
}
|
|
|
|
err := p.SavePost(ctx, post)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, post.ID)
|
|
|
|
posts, err := p.SelectPostsOfSite(ctx, site.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, posts, 1)
|
|
assert.Equal(t, post.ID, posts[0].ID)
|
|
assert.Equal(t, site.ID, posts[0].SiteID)
|
|
assert.Equal(t, "post-001", posts[0].GUID)
|
|
assert.Equal(t, "First Post", posts[0].Title)
|
|
assert.Equal(t, "Hello world", posts[0].Body)
|
|
assert.Equal(t, "/2026/02/19/first-post", posts[0].Slug)
|
|
assert.Equal(t, now, posts[0].CreatedAt)
|
|
assert.Equal(t, now, posts[0].PublishedAt)
|
|
})
|
|
|
|
t.Run("posts ordered by created_at desc", func(t *testing.T) {
|
|
// Create a second site to isolate this test
|
|
site2 := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "Second Blog",
|
|
Tagline: "",
|
|
}
|
|
require.NoError(t, p.SaveSite(ctx, site2))
|
|
|
|
earlier := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
later := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
post1 := &models.Post{
|
|
SiteID: site2.ID,
|
|
GUID: "old-post",
|
|
Title: "Old Post",
|
|
Body: "old",
|
|
Slug: "/old",
|
|
CreatedAt: earlier,
|
|
PublishedAt: earlier,
|
|
}
|
|
post2 := &models.Post{
|
|
SiteID: site2.ID,
|
|
GUID: "new-post",
|
|
Title: "New Post",
|
|
Body: "new",
|
|
Slug: "/new",
|
|
CreatedAt: later,
|
|
PublishedAt: later,
|
|
}
|
|
|
|
require.NoError(t, p.SavePost(ctx, post1))
|
|
require.NoError(t, p.SavePost(ctx, post2))
|
|
|
|
posts, err := p.SelectPostsOfSite(ctx, site2.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, posts, 2)
|
|
assert.Equal(t, "New Post", posts[0].Title)
|
|
assert.Equal(t, "Old Post", posts[1].Title)
|
|
})
|
|
|
|
t.Run("select posts for site with no posts", func(t *testing.T) {
|
|
emptySite := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "Empty Blog",
|
|
Tagline: "",
|
|
}
|
|
require.NoError(t, p.SaveSite(ctx, emptySite))
|
|
|
|
posts, err := p.SelectPostsOfSite(ctx, emptySite.ID)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, posts)
|
|
})
|
|
}
|
|
|
|
func TestProvider_PublishTargets(t *testing.T) {
|
|
ctx := context.Background()
|
|
p := newTestDB(t)
|
|
|
|
// Create user and site
|
|
user := &models.User{
|
|
Username: "testuser",
|
|
PasswordHashed: []byte("password"),
|
|
}
|
|
require.NoError(t, p.SaveUser(ctx, user))
|
|
|
|
site := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "My Blog",
|
|
Tagline: "A test blog",
|
|
}
|
|
require.NoError(t, p.SaveSite(ctx, site))
|
|
|
|
t.Run("save and select publish targets", func(t *testing.T) {
|
|
target := &models.SitePublishTarget{
|
|
SiteID: site.ID,
|
|
TargetType: models.PublishTargetTypeNetlify,
|
|
BaseURL: "https://example.netlify.app",
|
|
TargetRef: "netlify-site-123",
|
|
TargetKey: "secret-key",
|
|
}
|
|
|
|
err := p.SavePublishTarget(ctx, target)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, target.ID)
|
|
|
|
targets, err := p.SelectPublishTargetsOfSite(ctx, site.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, targets, 1)
|
|
assert.Equal(t, target.ID, targets[0].ID)
|
|
assert.Equal(t, site.ID, targets[0].SiteID)
|
|
assert.Equal(t, models.PublishTargetTypeNetlify, targets[0].TargetType)
|
|
assert.Equal(t, "https://example.netlify.app", targets[0].BaseURL)
|
|
assert.Equal(t, "netlify-site-123", targets[0].TargetRef)
|
|
assert.Equal(t, "secret-key", targets[0].TargetKey)
|
|
})
|
|
|
|
t.Run("select targets for site with no targets", func(t *testing.T) {
|
|
emptySite := &models.Site{
|
|
OwnerID: user.ID,
|
|
Title: "No Targets",
|
|
Tagline: "",
|
|
}
|
|
require.NoError(t, p.SaveSite(ctx, emptySite))
|
|
|
|
targets, err := p.SelectPublishTargetsOfSite(ctx, emptySite.ID)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, targets)
|
|
})
|
|
}
|
|
|
|
// Verify that password encoding roundtrips correctly through base64
|
|
func TestProvider_UserPasswordEncoding(t *testing.T) {
|
|
ctx := context.Background()
|
|
p := newTestDB(t)
|
|
|
|
// Use bytes that aren't valid UTF-8 to verify binary safety
|
|
rawPassword := []byte{0x00, 0xff, 0x80, 0x7f, 0x01}
|
|
|
|
user := &models.User{
|
|
Username: "binuser",
|
|
PasswordHashed: rawPassword,
|
|
}
|
|
require.NoError(t, p.SaveUser(ctx, user))
|
|
|
|
got, err := p.SelectUserByUsername(ctx, "binuser")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, rawPassword, got.PasswordHashed)
|
|
|
|
// Verify it's stored as base64 (not raw bytes) - this is implicit
|
|
// from the implementation but good to confirm the roundtrip works
|
|
_ = base64.StdEncoding.EncodeToString(rawPassword)
|
|
}
|