* refactor: update api and client to reference humanIds * fix: prevent deletion of network member This may cause various side effects if there is data in other services which reference this member
348 lines
11 KiB
Go
348 lines
11 KiB
Go
package particle_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/flowy-live/llink/internal/network"
|
|
"github.com/flowy-live/llink/internal/particle"
|
|
"github.com/flowy-live/llink/internal/testhelper"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
var dbPool *pgxpool.Pool
|
|
|
|
func TestMain(m *testing.M) {
|
|
dbPool = testhelper.SetupTestDB()
|
|
defer testhelper.TeardownTestDB()
|
|
|
|
ret := m.Run()
|
|
os.Exit(ret)
|
|
}
|
|
|
|
func getStreamStatus(data json.RawMessage) string {
|
|
var d struct {
|
|
Status string `json:"status"`
|
|
}
|
|
json.Unmarshal(data, &d)
|
|
return d.Status
|
|
}
|
|
|
|
func TestParticleService_CreateAndGet(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network first
|
|
net, err := networkSvc.Create(ctx, "Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Test Create stream particle
|
|
data := json.RawMessage(`{"name":"My Stream","status":"open","description":"A test stream"}`)
|
|
input := particle.CreateInput{
|
|
Type: particle.TypeStream,
|
|
NetworkID: net.ID,
|
|
Data: data,
|
|
}
|
|
created, err := svc.Create(ctx, input, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, created.ID)
|
|
assert.Equal(t, particle.TypeStream, created.Type)
|
|
assert.Equal(t, net.ID, created.NetworkID)
|
|
assert.Nil(t, created.ParentID)
|
|
assert.Equal(t, particle.VisibilityNetworkAll, created.Visibility)
|
|
assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(created.Data))
|
|
|
|
// Test GetByID
|
|
found, err := svc.GetByID(ctx, created.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, created.ID, found.ID)
|
|
|
|
// Test GetByID with non-existent id
|
|
_, err = svc.GetByID(ctx, "particle_nonexistent", "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrNotFound)
|
|
|
|
// Note: Network membership check is handler's responsibility
|
|
// Service assumes caller is already verified as network member
|
|
}
|
|
|
|
func TestParticleService_NestedParticles(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network
|
|
net, err := networkSvc.Create(ctx, "Nested Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create a parent stream
|
|
streamInput := particle.CreateInput{
|
|
Type: particle.TypeStream,
|
|
NetworkID: net.ID,
|
|
Data: json.RawMessage(`{"name":"Parent Stream","status":"open"}`),
|
|
}
|
|
stream, err := svc.Create(ctx, streamInput, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create a text particle as child
|
|
textInput := particle.CreateInput{
|
|
Type: particle.TypeText,
|
|
NetworkID: net.ID,
|
|
ParentID: &stream.ID,
|
|
Data: json.RawMessage(`{"content":"Hello world"}`),
|
|
}
|
|
text, err := svc.Create(ctx, textInput, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, stream.ID, *text.ParentID)
|
|
|
|
// Create a file as child of stream
|
|
fileInput := particle.CreateInput{
|
|
Type: particle.TypeFile,
|
|
NetworkID: net.ID,
|
|
ParentID: &stream.ID,
|
|
Data: json.RawMessage(`{"object_id":"obj_abc123","filename":"test.pdf","mime_type":"application/pdf","size":1024}`),
|
|
}
|
|
file, err := svc.Create(ctx, fileInput, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, stream.ID, *file.ParentID)
|
|
|
|
// List children of stream
|
|
children, err := svc.List(ctx, net.ID, &stream.ID, "[email protected]", particle.ListFilter{}, nil, 50)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, children.Particles, 2)
|
|
}
|
|
|
|
func TestParticleService_CustomVisibility(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network with a member
|
|
net, err := networkSvc.Create(ctx, "Visibility Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
err = networkSvc.AddMembers(ctx, net.ID, []string{"[email protected]", "[email protected]"})
|
|
assert.NoError(t, err)
|
|
|
|
// Create a stream with custom visibility including only admin and member
|
|
streamInput := particle.CreateInput{
|
|
Type: particle.TypeStream,
|
|
NetworkID: net.ID,
|
|
Visibility: particle.VisibilityCustom,
|
|
Members: []string{"[email protected]", "[email protected]"},
|
|
Data: json.RawMessage(`{"name":"Private Stream","status":"open"}`),
|
|
}
|
|
stream, err := svc.Create(ctx, streamInput, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Admin can access
|
|
_, err = svc.GetByID(ctx, stream.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Member can access
|
|
_, err = svc.GetByID(ctx, stream.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Other network member cannot access
|
|
_, err = svc.GetByID(ctx, stream.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrAccessDenied)
|
|
|
|
// Non-network member cannot access
|
|
_, err = svc.GetByID(ctx, stream.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrAccessDenied)
|
|
}
|
|
|
|
func TestParticleService_UpdateAndDelete(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network
|
|
net, err := networkSvc.Create(ctx, "Update Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create a text particle
|
|
input := particle.CreateInput{
|
|
Type: particle.TypeText,
|
|
NetworkID: net.ID,
|
|
Data: json.RawMessage(`{"content":"Original content"}`),
|
|
}
|
|
created, err := svc.Create(ctx, input, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Update the particle
|
|
newData := json.RawMessage(`{"content":"Updated content"}`)
|
|
updated, err := svc.Update(ctx, created.ID, newData, "[email protected]")
|
|
assert.NoError(t, err)
|
|
// PostgreSQL normalizes JSON, so compare unmarshaled values
|
|
var expected, actual map[string]interface{}
|
|
json.Unmarshal(newData, &expected)
|
|
json.Unmarshal(updated.Data, &actual)
|
|
assert.Equal(t, expected, actual)
|
|
|
|
// Delete the particle
|
|
err = svc.Delete(ctx, created.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Verify it's gone
|
|
_, err = svc.GetByID(ctx, created.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrNotFound)
|
|
}
|
|
|
|
func TestParticleService_ListRootParticles(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network
|
|
net, err := networkSvc.Create(ctx, "List Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create multiple root particles
|
|
for i := 0; i < 3; i++ {
|
|
input := particle.CreateInput{
|
|
Type: particle.TypeStream,
|
|
NetworkID: net.ID,
|
|
Data: json.RawMessage(`{"name":"Stream","status":"open"}`),
|
|
}
|
|
_, err := svc.Create(ctx, input, "[email protected]")
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// List root particles (parentID = nil)
|
|
list, err := svc.List(ctx, net.ID, nil, "[email protected]", particle.ListFilter{}, nil, 50)
|
|
assert.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(list.Particles), 3)
|
|
}
|
|
|
|
func TestParticleService_OpenCloseStream(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network
|
|
net, err := networkSvc.Create(ctx, "Open Close Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create a stream
|
|
input := particle.CreateInput{
|
|
Type: particle.TypeStream,
|
|
NetworkID: net.ID,
|
|
Data: json.RawMessage(`{"name":"Test Stream","status":"open"}`),
|
|
}
|
|
stream, err := svc.Create(ctx, input, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(stream.Data))
|
|
|
|
// Close the stream
|
|
err = svc.CloseStream(ctx, stream.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Verify it's closed
|
|
found, err := svc.GetByID(ctx, stream.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, string(particle.StreamStatusClosed), getStreamStatus(found.Data))
|
|
|
|
// Try to close again - should error
|
|
err = svc.CloseStream(ctx, stream.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrStreamAlreadyClosed)
|
|
|
|
// Reopen the stream
|
|
err = svc.OpenStream(ctx, stream.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Verify it's open
|
|
found, err = svc.GetByID(ctx, stream.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, string(particle.StreamStatusOpen), getStreamStatus(found.Data))
|
|
|
|
// Try to open again - should error
|
|
err = svc.OpenStream(ctx, stream.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrStreamAlreadyOpen)
|
|
}
|
|
|
|
func TestParticleService_NotAStream(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network
|
|
net, err := networkSvc.Create(ctx, "Not Stream Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create a text particle
|
|
input := particle.CreateInput{
|
|
Type: particle.TypeText,
|
|
NetworkID: net.ID,
|
|
Data: json.RawMessage(`{"content":"Hello"}`),
|
|
}
|
|
text, err := svc.Create(ctx, input, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Try to open it as a stream
|
|
err = svc.OpenStream(ctx, text.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrNotAStream)
|
|
|
|
// Try to close it as a stream
|
|
err = svc.CloseStream(ctx, text.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrNotAStream)
|
|
}
|
|
|
|
func TestParticleService_AccessInheritance(t *testing.T) {
|
|
ctx := context.Background()
|
|
networkSvc := network.NewService(dbPool)
|
|
svc := particle.NewService(dbPool, networkSvc)
|
|
|
|
// Create a network with members
|
|
net, err := networkSvc.Create(ctx, "Access Inheritance Test Network", "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
err = networkSvc.AddMembers(ctx, net.ID, []string{"[email protected]", "[email protected]"})
|
|
assert.NoError(t, err)
|
|
|
|
// Create a stream with custom visibility (admin and member only)
|
|
streamInput := particle.CreateInput{
|
|
Type: particle.TypeStream,
|
|
NetworkID: net.ID,
|
|
Visibility: particle.VisibilityCustom,
|
|
Members: []string{"[email protected]", "[email protected]"},
|
|
Data: json.RawMessage(`{"name":"Private Stream","status":"open"}`),
|
|
}
|
|
stream, err := svc.Create(ctx, streamInput, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Create a child text (network_all visibility)
|
|
textInput := particle.CreateInput{
|
|
Type: particle.TypeText,
|
|
NetworkID: net.ID,
|
|
ParentID: &stream.ID,
|
|
Data: json.RawMessage(`{"content":"Child text"}`),
|
|
}
|
|
text, err := svc.Create(ctx, textInput, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Admin can access child
|
|
_, err = svc.GetByID(ctx, text.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Member can access child
|
|
_, err = svc.GetByID(ctx, text.ID, "[email protected]")
|
|
assert.NoError(t, err)
|
|
|
|
// Other cannot access child (even though child is network_all, parent restricts)
|
|
_, err = svc.GetByID(ctx, text.ID, "[email protected]")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, particle.ErrAccessDenied)
|
|
}
|