mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 19:09:19 +02:00
add database tests
This commit is contained in:
parent
925912f4de
commit
d76190245f
|
|
@ -16,6 +16,7 @@ require (
|
|||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.21.6
|
||||
github.com/google/go-github/v72 v72.0.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
|
|
|
|||
355
server/core/internal/db/data_source_test.go
Normal file
355
server/core/internal/db/data_source_test.go
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource"
|
||||
"github.com/oxynote/oxynote/server/core/internal/datasource/processor"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepDataSources(t *testing.T, db *DB, count int, fn func(int, *datasource.DataSource)) []*datasource.DataSource {
|
||||
t.Helper()
|
||||
|
||||
res := make([]*datasource.DataSource, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
for i := range count {
|
||||
ds := &datasource.DataSource{
|
||||
ID: xid.New(),
|
||||
Name: "Data Source " + strconv.Itoa(i),
|
||||
Type: datasource.TypePrometheus,
|
||||
URL: "http://prometheus-" + strconv.Itoa(i) + ".test",
|
||||
Credentials: processor.Credentials(`{"username":"user","password":"pass"}`),
|
||||
Status: processor.ConnectionStatusSuccess,
|
||||
// distinct timestamps keep the fetch order deterministic.
|
||||
CreatedAt: now.Add(-time.Duration(i) * time.Second),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, ds)
|
||||
}
|
||||
|
||||
if ds.OrganizationID == "" {
|
||||
ds.OrganizationID = prepOrganizations(t, db, 1)[0]
|
||||
}
|
||||
|
||||
res[i] = ds
|
||||
|
||||
safeCredentials, err := ds.Credentials.Encrypt(db.opts.DataSourceCredentialsSigningSecret)
|
||||
require.NoError(t, err)
|
||||
|
||||
q, args := db.builder.Insert("data_sources").
|
||||
SetMap(map[string]any{
|
||||
"id": ds.ID,
|
||||
"fk_organization_id": ds.OrganizationID,
|
||||
"name": ds.Name,
|
||||
"type": ds.Type,
|
||||
"url": ds.URL,
|
||||
"credentials": safeCredentials,
|
||||
"status": ds.Status,
|
||||
"created_at": ds.CreatedAt,
|
||||
"updated_at": ds.UpdatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err = db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertDataSource(t *testing.T) {
|
||||
stubDataSource := func(organizationID, name string) *datasource.DataSource {
|
||||
return &datasource.DataSource{
|
||||
ID: xid.New(),
|
||||
OrganizationID: organizationID,
|
||||
Name: name,
|
||||
Type: datasource.TypePrometheus,
|
||||
URL: "http://prometheus.test",
|
||||
Credentials: processor.Credentials(`{"token":"secret"}`),
|
||||
Status: processor.ConnectionStatusSuccess,
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
type tcase struct {
|
||||
InvalidSecret bool
|
||||
DataSource *datasource.DataSource
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Invalid signing secret": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
InvalidSecret: true,
|
||||
DataSource: stubDataSource(org, "Data Source 1"),
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Duplicate name within the organization": func(t *testing.T, db *DB) tcase {
|
||||
ds := prepDataSources(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
DataSource: stubDataSource(ds.OrganizationID, ds.Name),
|
||||
Err: errutil.New(
|
||||
http.StatusBadRequest,
|
||||
"data_source.duplicate_name",
|
||||
"name is already in use",
|
||||
),
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
DataSource: stubDataSource(org, "Data Source 1"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
if c.InvalidSecret {
|
||||
db.opts.DataSourceCredentialsSigningSecret = "bad"
|
||||
}
|
||||
|
||||
err := db.InsertDataSource(context.Background(), c.DataSource)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDataSource(context.Background(), c.DataSource.ID, c.DataSource.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, c.DataSource, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDataSource(t *testing.T) {
|
||||
type tcase struct {
|
||||
InvalidSecret bool
|
||||
DataSource *datasource.DataSource
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Invalid signing secret": func(t *testing.T, db *DB) tcase {
|
||||
ds := prepDataSources(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
InvalidSecret: true,
|
||||
DataSource: ds,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
ds := prepDataSources(t, db, 1, nil)[0]
|
||||
ds.Name = "Updated Data Source"
|
||||
ds.URL = "http://updated.test"
|
||||
ds.Credentials = processor.Credentials(`{"token":"rotated"}`)
|
||||
ds.Status = processor.ConnectionStatusSuccess
|
||||
ds.UpdatedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
|
||||
return tcase{
|
||||
DataSource: ds,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
if c.InvalidSecret {
|
||||
db.opts.DataSourceCredentialsSigningSecret = "bad"
|
||||
}
|
||||
|
||||
err := db.UpdateDataSource(context.Background(), c.DataSource)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDataSource(context.Background(), c.DataSource.ID, c.DataSource.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, c.DataSource, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDataSource(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
ID xid.ID
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
ds := prepDataSources(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
ID: ds.ID,
|
||||
OrganizationID: ds.OrganizationID,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
ds := prepDataSources(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
ID: ds.ID,
|
||||
OrganizationID: ds.OrganizationID,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDataSource(ctx, c.ID, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var id xid.ID
|
||||
|
||||
q, args := db.builder.Select("id").
|
||||
From("data_sources").
|
||||
Where(sq.Eq{
|
||||
"id": c.ID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&id, q, args...)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDataSource(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchDataSource(context.Background(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// error - undecryptable credentials
|
||||
ds := prepDataSources(t, db, 1, nil)[0]
|
||||
|
||||
q, args := db.builder.Update("data_sources").
|
||||
SetMap(map[string]any{
|
||||
"credentials": []byte("garbage"),
|
||||
}).
|
||||
Where(sq.Eq{
|
||||
"id": ds.ID,
|
||||
}).MustSql()
|
||||
|
||||
_, err = db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err = db.FetchDataSource(context.Background(), ds.ID, ds.OrganizationID)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
ds = prepDataSources(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchDataSource(context.Background(), ds.ID, ds.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, ds, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDataSources(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// success - no data sources
|
||||
res, err := db.FetchDataSources(context.Background(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err = db.FetchDataSources(ctx, "non-existent-org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// error - undecryptable credentials
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
ds := prepDataSources(t, db, 1, func(_ int, nds *datasource.DataSource) {
|
||||
nds.OrganizationID = org
|
||||
})[0]
|
||||
|
||||
q, args := db.builder.Update("data_sources").
|
||||
SetMap(map[string]any{
|
||||
"credentials": []byte("garbage"),
|
||||
}).
|
||||
Where(sq.Eq{
|
||||
"id": ds.ID,
|
||||
}).MustSql()
|
||||
|
||||
_, err = db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err = db.FetchDataSources(context.Background(), org)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - ordered by creation time, newest first
|
||||
org = prepOrganizations(t, db, 1)[0]
|
||||
sources := prepDataSources(t, db, 3, func(_ int, nds *datasource.DataSource) {
|
||||
nds.OrganizationID = org
|
||||
})
|
||||
|
||||
res, err = db.FetchDataSources(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, res, len(sources))
|
||||
|
||||
for i, ds := range sources {
|
||||
testutil.AssertFilterEqual(t, *ds, res[i])
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,12 @@ import (
|
|||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lann/builder"
|
||||
"github.com/orlangure/gnomock"
|
||||
pgDocker "github.com/orlangure/gnomock/preset/postgres"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/errutil"
|
||||
|
|
@ -28,6 +31,11 @@ var _pgDSN string
|
|||
const (
|
||||
_pgUser string = "pgtest"
|
||||
_pgPass string = "pgpass"
|
||||
|
||||
// _dataSourceSecret is the credentials signing secret used by
|
||||
// all temporary test databases. It must be a valid AES key
|
||||
// length (32 bytes).
|
||||
_dataSourceSecret string = "0123456789abcdef0123456789abcdef"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
|
@ -208,11 +216,32 @@ func Test_Tx_Rollback(t *testing.T) {
|
|||
require.Error(t, ntx.Rollback())
|
||||
}
|
||||
|
||||
// prepMockDB creates an agent backed by sqlmock for reaching error
|
||||
// branches that a real database cannot produce (e.g. failures in the
|
||||
// middle of a multi-statement transaction).
|
||||
func prepMockDB(t *testing.T) (*agent, sqlmock.Sqlmock) {
|
||||
t.Helper()
|
||||
|
||||
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
mockDB.Close() //nolint:errcheck,gosec // error provides no meaningful info
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
})
|
||||
|
||||
return &agent{
|
||||
sql: sqlx.NewDb(mockDB, "sqlmock"),
|
||||
builder: sq.StatementBuilderType(builder.EmptyBuilder).PlaceholderFormat(sq.Dollar),
|
||||
}, mock
|
||||
}
|
||||
|
||||
func prepTempDB(t *testing.T) *DB {
|
||||
name := uniuri.NewLenChars(10, []byte("abcdefghijklmnopqrstuvwxyz"))
|
||||
|
||||
opts := Options{
|
||||
DSN: fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", _pgUser, _pgPass, _pgDSN, name),
|
||||
DSN: fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", _pgUser, _pgPass, _pgDSN, name),
|
||||
DataSourceCredentialsSigningSecret: _dataSourceSecret,
|
||||
}
|
||||
|
||||
tmpDB, err := sqlx.Connect(
|
||||
|
|
|
|||
674
server/core/internal/db/document_branch_test.go
Normal file
674
server/core/internal/db/document_branch_test.go
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepDocumentBranches(t *testing.T, db *DB, count int, fn func(int, *document.Document)) []*document.Document {
|
||||
t.Helper()
|
||||
|
||||
res := make([]*document.Document, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
// branches missing a document after fn ran share one lazily
|
||||
// created parent, so a single call produces sibling branches.
|
||||
var parent *document.Document
|
||||
|
||||
for i := range count {
|
||||
doc := &document.Document{
|
||||
Branch: document.Branch{
|
||||
BranchID: xid.New(),
|
||||
BranchName: "branch-" + strconv.Itoa(i),
|
||||
DocumentName: "Branch Document " + strconv.Itoa(i),
|
||||
Icon: "icon-branch",
|
||||
Content: document.RootBlock{
|
||||
Type: "doc",
|
||||
Content: []document.Block{
|
||||
{
|
||||
Type: "paragraph",
|
||||
Text: "Branch content " + strconv.Itoa(i),
|
||||
},
|
||||
},
|
||||
},
|
||||
RawContent: []byte("Branch content " + strconv.Itoa(i)),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, doc)
|
||||
}
|
||||
|
||||
if doc.ID.IsZero() {
|
||||
if parent == nil {
|
||||
parent = prepDocuments(t, db, 1, nil)[0]
|
||||
}
|
||||
|
||||
doc.ID = parent.ID
|
||||
doc.OrganizationID = parent.OrganizationID
|
||||
}
|
||||
|
||||
res[i] = doc
|
||||
|
||||
q, args := db.builder.Insert("document_branches").
|
||||
SetMap(map[string]any{
|
||||
"id": doc.BranchID,
|
||||
"fk_document_id": doc.ID,
|
||||
"fk_organization_id": doc.OrganizationID,
|
||||
"branch_name": doc.BranchName,
|
||||
"document_name": doc.DocumentName,
|
||||
"icon": doc.Icon,
|
||||
"content": doc.Content,
|
||||
"raw_content": doc.RawContent,
|
||||
"protected": doc.Protected,
|
||||
`"default"`: doc.Default,
|
||||
"created_at": doc.CreatedAt,
|
||||
"fk_created_by": doc.CreatedBy,
|
||||
"updated_at": doc.UpdatedAt,
|
||||
"fk_last_updated_by": doc.LastUpdatedBy,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_insertDocumentBranch(t *testing.T) {
|
||||
type tcase struct {
|
||||
Document document.Document
|
||||
Original *document.Document
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Duplicate branch ID": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
doc.BranchID = branch.BranchID
|
||||
doc.BranchName = "another-branch"
|
||||
|
||||
return tcase{
|
||||
Document: *doc,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Existing branch is left untouched": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
doc := *branch
|
||||
doc.BranchID = xid.New()
|
||||
doc.DocumentName = "Should Not Overwrite"
|
||||
|
||||
return tcase{
|
||||
Document: doc,
|
||||
Original: branch,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
doc.BranchID = xid.New()
|
||||
doc.BranchName = "feature-x"
|
||||
|
||||
return tcase{
|
||||
Document: *doc,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.insertDocumentBranch(context.Background(), c.Document)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exp := &c.Document
|
||||
|
||||
if c.Original != nil {
|
||||
exp = c.Original
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentByBranchID(context.Background(), exp.BranchID, exp.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, exp, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_upsertDocumentBranch(t *testing.T) {
|
||||
type tcase struct {
|
||||
Document document.Document
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent document": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
Document: document.Document{
|
||||
ID: xid.New(),
|
||||
OrganizationID: "non-existent-org-id",
|
||||
Branch: document.Branch{
|
||||
BranchID: xid.New(),
|
||||
BranchName: "feature-x",
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
UpdatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert of a new branch": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
doc.BranchID = xid.New()
|
||||
doc.BranchName = "feature-x"
|
||||
|
||||
return tcase{
|
||||
Document: *doc,
|
||||
}
|
||||
},
|
||||
"Successful update of an existing branch": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
doc.DocumentName = "Updated Name"
|
||||
doc.Icon = "updated-icon"
|
||||
doc.RawContent = []byte("updated content")
|
||||
doc.Protected = true
|
||||
doc.UpdatedAt = timeutil.Now().Truncate(time.Second).Add(time.Hour)
|
||||
|
||||
return tcase{
|
||||
Document: *doc,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.upsertDocumentBranch(context.Background(), c.Document)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentByBranchID(context.Background(), c.Document.BranchID, c.Document.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Document, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_ForkDocumentBranch(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := db.ForkDocumentBranch(ctx, xid.New(), "org-id", document.DefaultBranch, "feature-x", "user-1")
|
||||
require.Error(t, err)
|
||||
|
||||
// success - source branch does not exist, nothing inserted
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
err = db.ForkDocumentBranch(context.Background(), doc.ID, doc.OrganizationID, "non-existent-branch", "feature-x", "user-1")
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := db.CountDocumentBranches(context.Background(), doc.ID, doc.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
|
||||
// success - fork of the default branch
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
err = db.ForkDocumentBranch(context.Background(), doc.ID, doc.OrganizationID, document.DefaultBranch, "feature-x", users[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
var forked document.Document
|
||||
|
||||
q, args := db.builder.Select(
|
||||
`id AS "branch_id"`,
|
||||
`branch_name AS "branch_name"`,
|
||||
`document_name AS "document_name"`,
|
||||
`icon AS "icon"`,
|
||||
`content AS "content"`,
|
||||
`raw_content AS "raw_content"`,
|
||||
`protected AS "protected"`,
|
||||
`"default" AS "default"`,
|
||||
`fk_created_by AS "fk_created_by"`,
|
||||
`fk_last_updated_by AS "fk_last_updated_by"`,
|
||||
).From("document_branches").
|
||||
Where(sq.Eq{
|
||||
"fk_document_id": doc.ID,
|
||||
"branch_name": "feature-x",
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&forked, q, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, doc.DocumentName, forked.DocumentName)
|
||||
assert.Equal(t, doc.Icon, forked.Icon)
|
||||
assert.Equal(t, doc.Content, forked.Content)
|
||||
assert.Equal(t, doc.RawContent, forked.RawContent)
|
||||
assert.False(t, forked.Default)
|
||||
assert.Equal(t, null.StringFrom(users[0]), forked.CreatedBy)
|
||||
assert.Equal(t, null.StringFrom(users[0]), forked.LastUpdatedBy)
|
||||
|
||||
// success - target branch already exists, nothing overwritten
|
||||
err = db.ForkDocumentBranch(context.Background(), doc.ID, doc.OrganizationID, document.DefaultBranch, "feature-x", "someone-else")
|
||||
require.NoError(t, err)
|
||||
|
||||
var createdBy null.String
|
||||
|
||||
q, args = db.builder.Select("fk_created_by").
|
||||
From("document_branches").
|
||||
Where(sq.Eq{
|
||||
"fk_document_id": doc.ID,
|
||||
"branch_name": "feature-x",
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&createdBy, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(users[0]), createdBy)
|
||||
}
|
||||
|
||||
func Test_agent_FetchMainBranchContent(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchMainBranchContent(context.Background(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Zero(t, res)
|
||||
|
||||
// success
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchMainBranchContent(context.Background(), doc.ID, doc.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, document.Content{
|
||||
OrganizationID: doc.OrganizationID,
|
||||
DocumentID: doc.ID,
|
||||
DocumentName: doc.DocumentName,
|
||||
Content: doc.Content,
|
||||
}, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentBranches(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentBranches(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no branches
|
||||
res, err = db.FetchDocumentBranches(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
branch := prepDocumentBranches(t, db, 1, func(_ int, ndoc *document.Document) {
|
||||
ndoc.ID = doc.ID
|
||||
ndoc.OrganizationID = doc.OrganizationID
|
||||
})[0]
|
||||
|
||||
exp := []document.BranchSummary{
|
||||
{
|
||||
BranchID: doc.BranchID,
|
||||
BranchName: doc.BranchName,
|
||||
DocumentName: doc.DocumentName,
|
||||
Icon: doc.Icon,
|
||||
Protected: doc.Protected,
|
||||
Default: doc.Default,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
},
|
||||
{
|
||||
BranchID: branch.BranchID,
|
||||
BranchName: branch.BranchName,
|
||||
DocumentName: branch.DocumentName,
|
||||
Icon: branch.Icon,
|
||||
Protected: branch.Protected,
|
||||
Default: branch.Default,
|
||||
CreatedAt: branch.CreatedAt,
|
||||
UpdatedAt: branch.UpdatedAt,
|
||||
},
|
||||
}
|
||||
|
||||
res, err = db.FetchDocumentBranches(context.Background(), doc.ID, doc.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, exp, res)
|
||||
}
|
||||
|
||||
func Test_agent_CountDocumentBranches(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
count, err := db.CountDocumentBranches(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Zero(t, count)
|
||||
|
||||
// success - no branches
|
||||
count, err = db.CountDocumentBranches(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, count)
|
||||
|
||||
// success
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
prepDocumentBranches(t, db, 2, func(_ int, ndoc *document.Document) {
|
||||
ndoc.ID = doc.ID
|
||||
ndoc.OrganizationID = doc.OrganizationID
|
||||
})
|
||||
|
||||
count, err = db.CountDocumentBranches(context.Background(), doc.ID, doc.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, count)
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocumentBranchByID(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
BranchID xid.ID
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
BranchID: branch.BranchID,
|
||||
OrganizationID: branch.OrganizationID,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
BranchID: branch.BranchID,
|
||||
OrganizationID: branch.OrganizationID,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDocumentBranchByID(ctx, c.BranchID, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var id xid.ID
|
||||
|
||||
q, args := db.builder.Select("id").
|
||||
From("document_branches").
|
||||
Where(sq.Eq{
|
||||
"id": c.BranchID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&id, q, args...)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDocumentBranchMetadata(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Document document.Document
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Document: *branch,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
branch.BranchName = "renamed-branch"
|
||||
branch.Protected = true
|
||||
branch.UpdatedAt = branch.UpdatedAt.Add(time.Hour)
|
||||
branch.LastUpdatedBy = null.StringFrom(users[0])
|
||||
|
||||
return tcase{
|
||||
Document: *branch,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateDocumentBranchMetadata(ctx, c.Document)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentByBranchID(context.Background(), c.Document.BranchID, c.Document.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Document, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentByBranchID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchDocumentByBranchID(context.Background(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchDocumentByBranchID(context.Background(), branch.BranchID, branch.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, branch, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentBranchesUnsafe(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentBranchesUnsafe(ctx, xid.New())
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no branches
|
||||
res, err = db.FetchDocumentBranchesUnsafe(context.Background(), xid.New())
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success - no organization scoping applied
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
exp := []document.BranchSummary{
|
||||
{
|
||||
BranchID: doc.BranchID,
|
||||
BranchName: doc.BranchName,
|
||||
DocumentName: doc.DocumentName,
|
||||
Icon: doc.Icon,
|
||||
Protected: doc.Protected,
|
||||
Default: doc.Default,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
},
|
||||
}
|
||||
|
||||
res, err = db.FetchDocumentBranchesUnsafe(context.Background(), doc.ID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, exp, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentUnsafeByBranchID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchDocumentUnsafeByBranchID(context.Background(), xid.New())
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no organization scoping applied
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchDocumentUnsafeByBranchID(context.Background(), branch.BranchID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, branch, res)
|
||||
}
|
||||
|
||||
func Test_agent_insertDocumentBranchChangelog(t *testing.T) {
|
||||
type tcase struct {
|
||||
DocumentID xid.ID
|
||||
BranchName string
|
||||
Changelog document.Changelog
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent branch": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
clog := doc.Changelog()
|
||||
|
||||
return tcase{
|
||||
DocumentID: doc.ID,
|
||||
BranchName: "non-existent-branch",
|
||||
Changelog: clog,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
DocumentID: doc.ID,
|
||||
BranchName: doc.BranchName,
|
||||
Changelog: doc.Changelog(),
|
||||
}
|
||||
},
|
||||
"Successful update of an existing entry": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
require.NoError(t, db.insertDocumentBranchChangelog(
|
||||
context.Background(),
|
||||
doc.ID,
|
||||
doc.BranchName,
|
||||
doc.Changelog(),
|
||||
))
|
||||
|
||||
doc.RawContent = []byte("updated raw content")
|
||||
|
||||
return tcase{
|
||||
DocumentID: doc.ID,
|
||||
BranchName: doc.BranchName,
|
||||
Changelog: doc.Changelog(),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.insertDocumentBranchChangelog(context.Background(), c.DocumentID, c.BranchName, c.Changelog)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var clog document.Changelog
|
||||
|
||||
q, args := db.builder.Select(
|
||||
`id AS "id"`,
|
||||
`fk_document_id AS "fk_document_id"`,
|
||||
`content AS "content"`,
|
||||
`raw_content AS "raw_content"`,
|
||||
`created_at AS "created_at"`,
|
||||
).From("document_branch_changelogs").
|
||||
Where(sq.Eq{
|
||||
"id": c.Changelog.ID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&clog, q, args...)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, c.Changelog, clog)
|
||||
})
|
||||
}
|
||||
}
|
||||
726
server/core/internal/db/document_comment_test.go
Normal file
726
server/core/internal/db/document_comment_test.go
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/comment"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepDocumentComments(t *testing.T, db *DB, count int, fn func(int, *comment.Comment)) []comment.Comment { //nolint:unparam // fixture helpers share a uniform signature
|
||||
t.Helper()
|
||||
|
||||
res := make([]comment.Comment, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
// comments missing a branch after fn ran share one lazily created
|
||||
// branch, so a single call produces comments of the same branch.
|
||||
var branch *document.Document
|
||||
|
||||
for i := range count {
|
||||
c := comment.Comment{
|
||||
ID: xid.New(),
|
||||
Content: comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "Comment " + strconv.Itoa(i),
|
||||
},
|
||||
// distinct timestamps keep the fetch order deterministic.
|
||||
CreatedAt: now.Add(-time.Duration(i) * time.Second),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &c)
|
||||
}
|
||||
|
||||
if c.BranchID.IsZero() {
|
||||
if branch == nil {
|
||||
branch = prepDocumentBranches(t, db, 1, nil)[0]
|
||||
}
|
||||
|
||||
c.BranchID = branch.BranchID
|
||||
c.DocumentID = branch.ID
|
||||
c.OrganizationID = branch.OrganizationID
|
||||
}
|
||||
|
||||
res[i] = c
|
||||
|
||||
q, args := db.builder.Insert("document_comments").
|
||||
SetMap(map[string]any{
|
||||
"id": c.ID,
|
||||
"fk_document_id": c.DocumentID,
|
||||
"fk_organization_id": c.OrganizationID,
|
||||
"fk_user_id": c.UserID,
|
||||
"fk_branch_id": c.BranchID,
|
||||
"resolved": c.Resolved,
|
||||
"fk_resolved_by": c.ResolvedBy,
|
||||
"anchor_block_id": c.AnchorBlockID,
|
||||
"content": c.Content,
|
||||
"diff_deletion_context": c.DiffDeletionContext,
|
||||
"created_at": c.CreatedAt,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func prepDocumentCommentReplies(t *testing.T, db *DB, count int, fn func(int, *comment.Reply)) []comment.Reply {
|
||||
t.Helper()
|
||||
|
||||
res := make([]comment.Reply, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
// replies missing a comment after fn ran share one lazily created
|
||||
// comment, so a single call produces replies of the same comment.
|
||||
var parent *comment.Comment
|
||||
|
||||
for i := range count {
|
||||
r := comment.Reply{
|
||||
ID: xid.New(),
|
||||
Content: comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "Reply " + strconv.Itoa(i),
|
||||
},
|
||||
// distinct ascending timestamps keep the fetch order
|
||||
// deterministic.
|
||||
CreatedAt: now.Add(time.Duration(i) * time.Second),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &r)
|
||||
}
|
||||
|
||||
if r.CommentID.IsZero() {
|
||||
if parent == nil {
|
||||
parent = &prepDocumentComments(t, db, 1, nil)[0]
|
||||
}
|
||||
|
||||
r.CommentID = parent.ID
|
||||
r.OrganizationID = parent.OrganizationID
|
||||
}
|
||||
|
||||
res[i] = r
|
||||
|
||||
q, args := db.builder.Insert("document_comment_replies").
|
||||
SetMap(map[string]any{
|
||||
"id": r.ID,
|
||||
"fk_document_comment_id": r.CommentID,
|
||||
"fk_organization_id": r.OrganizationID,
|
||||
"fk_user_id": r.UserID,
|
||||
"content": r.Content,
|
||||
"created_at": r.CreatedAt,
|
||||
"updated_at": r.UpdatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertDocumentComment(t *testing.T) {
|
||||
type tcase struct {
|
||||
Comment comment.Comment
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent branch": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
Comment: comment.Comment{
|
||||
ID: xid.New(),
|
||||
DocumentID: xid.New(),
|
||||
OrganizationID: "non-existent-org-id",
|
||||
BranchID: xid.New(),
|
||||
Content: comment.Content{
|
||||
"text": "hello",
|
||||
},
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
Comment: comment.Comment{
|
||||
ID: xid.New(),
|
||||
DocumentID: branch.ID,
|
||||
OrganizationID: branch.OrganizationID,
|
||||
BranchID: branch.BranchID,
|
||||
AnchorBlockID: null.StringFrom("block-1"),
|
||||
UserID: null.StringFrom(users[0]),
|
||||
Content: comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "A fresh comment",
|
||||
},
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
Replies: []comment.Reply{},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertDocumentComment(context.Background(), c.Comment)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentComment(context.Background(), c.Comment.ID, c.Comment.DocumentID, c.Comment.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Comment, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_InsertDocumentCommentReply(t *testing.T) {
|
||||
type tcase struct {
|
||||
Reply comment.Reply
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent comment": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
Reply: comment.Reply{
|
||||
ID: xid.New(),
|
||||
CommentID: xid.New(),
|
||||
OrganizationID: "non-existent-org-id",
|
||||
Content: comment.Content{
|
||||
"text": "hello",
|
||||
},
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
Reply: comment.Reply{
|
||||
ID: xid.New(),
|
||||
CommentID: c.ID,
|
||||
OrganizationID: c.OrganizationID,
|
||||
UserID: null.StringFrom(users[0]),
|
||||
Content: comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "A fresh reply",
|
||||
},
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertDocumentCommentReply(context.Background(), c.Reply)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentCommentReply(context.Background(), c.Reply.ID, c.Reply.CommentID, c.Reply.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Reply, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentComment(t *testing.T) {
|
||||
t.Run("Not found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
res, err := db.FetchDocumentComment(context.Background(), xid.New(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the replies query", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
id := xid.New()
|
||||
|
||||
mock.ExpectQuery("SELECT .* FROM document_comments").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id.String()))
|
||||
mock.ExpectQuery("SELECT .* FROM document_comment_replies").
|
||||
WillReturnError(assert.AnError)
|
||||
|
||||
res, err := a.FetchDocumentComment(context.Background(), id, xid.New(), "org-id")
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
|
||||
t.Run("Successful fetch with replies", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
c.Replies = prepDocumentCommentReplies(t, db, 2, func(_ int, r *comment.Reply) {
|
||||
r.CommentID = c.ID
|
||||
r.OrganizationID = c.OrganizationID
|
||||
})
|
||||
|
||||
res, err := db.FetchDocumentComment(context.Background(), c.ID, c.DocumentID, c.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c, res)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentCommentReply(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchDocumentCommentReply(context.Background(), xid.New(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
r := prepDocumentCommentReplies(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchDocumentCommentReply(context.Background(), r.ID, r.CommentID, r.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &r, res)
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDocumentComment(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Comment comment.Comment
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Comment: c,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
c.Content = comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "Updated comment",
|
||||
}
|
||||
c.Resolved = true
|
||||
c.ResolvedBy = null.StringFrom(users[0])
|
||||
c.AnchorBlockID = null.StringFrom("block-2")
|
||||
c.UpdatedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
c.Replies = []comment.Reply{}
|
||||
|
||||
return tcase{
|
||||
Comment: c,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateDocumentComment(ctx, c.Comment)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentComment(context.Background(), c.Comment.ID, c.Comment.DocumentID, c.Comment.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Comment, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDocumentCommentReply(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Reply comment.Reply
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
r := prepDocumentCommentReplies(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Reply: r,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
r := prepDocumentCommentReplies(t, db, 1, nil)[0]
|
||||
r.Content = comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "Updated reply",
|
||||
}
|
||||
r.UpdatedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
|
||||
return tcase{
|
||||
Reply: r,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateDocumentCommentReply(ctx, c.Reply)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentCommentReply(context.Background(), c.Reply.ID, c.Reply.CommentID, c.Reply.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Reply, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocumentComment(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Comment comment.Comment
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Comment: c,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Comment: c,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDocumentComment(ctx, c.Comment.ID, c.Comment.DocumentID, c.Comment.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentComment(context.Background(), c.Comment.ID, c.Comment.DocumentID, c.Comment.OrganizationID)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocumentCommentReply(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Reply comment.Reply
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
r := prepDocumentCommentReplies(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Reply: r,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
r := prepDocumentCommentReplies(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Reply: r,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDocumentCommentReply(ctx, c.Reply.ID, c.Reply.CommentID, c.Reply.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentCommentReply(context.Background(), c.Reply.ID, c.Reply.CommentID, c.Reply.OrganizationID)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_ReplaceDocumentComment(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Comment comment.Comment
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Comment: c,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful replace": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
c := prepDocumentComments(t, db, 1, nil)[0]
|
||||
c.Content = comment.Content{
|
||||
"type": "paragraph",
|
||||
"text": "Promoted reply content",
|
||||
}
|
||||
c.UserID = null.StringFrom(users[0])
|
||||
c.AnchorBlockID = null.StringFrom("block-3")
|
||||
c.CreatedAt = c.CreatedAt.Add(time.Hour)
|
||||
c.UpdatedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
c.Replies = []comment.Reply{}
|
||||
|
||||
return tcase{
|
||||
Comment: c,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.ReplaceDocumentComment(ctx, c.Comment)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentComment(context.Background(), c.Comment.ID, c.Comment.DocumentID, c.Comment.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Comment, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentCommentsByBranchID(t *testing.T) {
|
||||
t.Run("Error returned by the comments query", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentCommentsByBranchID(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the replies query", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
id := xid.New()
|
||||
|
||||
mock.ExpectQuery("SELECT .* FROM document_comments").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id.String()))
|
||||
mock.ExpectQuery("SELECT .* FROM document_comment_replies").
|
||||
WillReturnError(assert.AnError)
|
||||
|
||||
res, err := a.FetchDocumentCommentsByBranchID(context.Background(), xid.New(), "org-id")
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
|
||||
t.Run("Successful fetch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
// success - no comments
|
||||
res, err := db.FetchDocumentCommentsByBranchID(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success - newest comment first, replies in creation order
|
||||
comments := prepDocumentComments(t, db, 2, nil)
|
||||
comments[0].Replies = prepDocumentCommentReplies(t, db, 2, func(_ int, r *comment.Reply) {
|
||||
r.CommentID = comments[0].ID
|
||||
r.OrganizationID = comments[0].OrganizationID
|
||||
})
|
||||
|
||||
res, err = db.FetchDocumentCommentsByBranchID(context.Background(), comments[0].BranchID, comments[0].OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, comments, res)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocumentCommentsByBranchID(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Comments []comment.Comment
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
comments := prepDocumentComments(t, db, 1, nil)
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Comments: comments,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
return tcase{
|
||||
Comments: prepDocumentComments(t, db, 2, nil),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDocumentCommentsByBranchID(ctx, c.Comments[0].BranchID, c.Comments[0].OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentCommentsByBranchID(context.Background(), c.Comments[0].BranchID, c.Comments[0].OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
124
server/core/internal/db/document_file_test.go
Normal file
124
server/core/internal/db/document_file_test.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepDocumentFiles(t *testing.T, db *DB, count int, fn func(int, *document.File)) []document.File {
|
||||
t.Helper()
|
||||
|
||||
res := make([]document.File, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
for i := range count {
|
||||
f := document.File{
|
||||
ID: "file-" + xid.New().String(),
|
||||
Location: document.LocationDocument,
|
||||
CreatedAt: now.Add(-time.Duration(i) * time.Second),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &f)
|
||||
}
|
||||
|
||||
if f.DocumentID.IsZero() {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
f.DocumentID = doc.ID
|
||||
f.OrganizationID = doc.OrganizationID
|
||||
}
|
||||
|
||||
res[i] = f
|
||||
|
||||
q, args := db.builder.Insert("document_files").
|
||||
SetMap(map[string]any{
|
||||
"id": f.ID,
|
||||
"location": f.Location,
|
||||
"fk_document_id": f.DocumentID,
|
||||
"fk_organization_id": f.OrganizationID,
|
||||
"created_at": f.CreatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertDocumentFile(t *testing.T) {
|
||||
type tcase struct {
|
||||
File document.File
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Duplicate file ID": func(t *testing.T, db *DB) tcase {
|
||||
f := prepDocumentFiles(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
File: f,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
File: document.File{
|
||||
ID: "file-1",
|
||||
Location: document.LocationComment,
|
||||
DocumentID: doc.ID,
|
||||
OrganizationID: doc.OrganizationID,
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertDocumentFile(context.Background(), c.File)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentFile(context.Background(), c.File.ID, c.File.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.File, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentFile(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchDocumentFile(context.Background(), "non-existent-file-id", "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
f := prepDocumentFiles(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchDocumentFile(context.Background(), f.ID, f.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &f, res)
|
||||
}
|
||||
392
server/core/internal/db/document_hook_test.go
Normal file
392
server/core/internal/db/document_hook_test.go
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/hook"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/hook/processor"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepDocumentHooks(t *testing.T, db *DB, count int, fn func(int, *hook.Hook)) []hook.Hook {
|
||||
t.Helper()
|
||||
|
||||
res := make([]hook.Hook, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
// hooks missing a document after fn ran share one lazily created
|
||||
// branch, so a single call produces hooks of the same document.
|
||||
var branch *document.Document
|
||||
|
||||
for i := range count {
|
||||
hk := hook.Hook{
|
||||
ID: xid.New(),
|
||||
Type: hook.TypeURLWatcher,
|
||||
BlockID: null.StringFrom("block-" + xid.New().String()),
|
||||
// JSONB round-trips in canonical form (space after the
|
||||
// colon), so the fixture uses that form for comparisons.
|
||||
Settings: processor.Settings(`{"url": "http://watched.test"}`),
|
||||
State: processor.State(`{"status":"ok"}`),
|
||||
Score: decimal.NewFromInt(50),
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &hk)
|
||||
}
|
||||
|
||||
if hk.DocumentID.IsZero() {
|
||||
if branch == nil {
|
||||
branch = prepDocumentBranches(t, db, 1, nil)[0]
|
||||
}
|
||||
|
||||
hk.DocumentID = branch.ID
|
||||
hk.OrganizationID = branch.OrganizationID
|
||||
hk.BranchID = null.ValueFrom(branch.BranchID)
|
||||
}
|
||||
|
||||
res[i] = hk
|
||||
|
||||
q, args := db.builder.Insert("document_hooks").
|
||||
SetMap(map[string]any{
|
||||
"id": hk.ID,
|
||||
"type": hk.Type,
|
||||
"fk_document_id": hk.DocumentID,
|
||||
"fk_organization_id": hk.OrganizationID,
|
||||
"fk_branch_id": hk.BranchID,
|
||||
"block_id": hk.BlockID,
|
||||
"settings": hk.Settings,
|
||||
"state": hk.State,
|
||||
"score": hk.Score,
|
||||
"created_at": hk.CreatedAt,
|
||||
"updated_at": hk.UpdatedAt,
|
||||
"soft_deleted_at": hk.SoftDeletedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertDocumentHook(t *testing.T) {
|
||||
type tcase struct {
|
||||
Hook hook.Hook
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Duplicate hook ID": func(t *testing.T, db *DB) tcase {
|
||||
hk := prepDocumentHooks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Hook: hk,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Hook: hook.Hook{
|
||||
ID: xid.New(),
|
||||
Type: hook.TypeScheduledReminder,
|
||||
DocumentID: branch.ID,
|
||||
OrganizationID: branch.OrganizationID,
|
||||
BranchID: null.ValueFrom(branch.BranchID),
|
||||
Settings: processor.Settings(`{"schedule": "0 0 * * * *"}`),
|
||||
State: processor.State(`{}`),
|
||||
Score: decimal.NewFromInt(100),
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
UpdatedAt: null.TimeFrom(timeutil.Now().Truncate(time.Second)),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertDocumentHook(context.Background(), c.Hook)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentHook(context.Background(), c.Hook.ID, c.Hook.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Hook, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentHook(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchDocumentHook(context.Background(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
hk := prepDocumentHooks(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchDocumentHook(context.Background(), hk.ID, hk.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &hk, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentHooksByDocumentID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentHooksByDocumentID(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no hooks
|
||||
res, err = db.FetchDocumentHooksByDocumentID(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success
|
||||
hooks := prepDocumentHooks(t, db, 2, nil)
|
||||
|
||||
res, err = db.FetchDocumentHooksByDocumentID(context.Background(), hooks[0].DocumentID, hooks[0].OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, hooks, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchPaginatedDocumentHooks(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchPaginatedDocumentHooks(ctx, xid.NilID(), 10)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no hooks
|
||||
res, err = db.FetchPaginatedDocumentHooks(context.Background(), xid.NilID(), 10)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success - first page
|
||||
hooks := prepDocumentHooks(t, db, 3, nil)
|
||||
|
||||
res, err = db.FetchPaginatedDocumentHooks(context.Background(), xid.NilID(), 2)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, hooks[:2], res)
|
||||
|
||||
// success - second page
|
||||
res, err = db.FetchPaginatedDocumentHooks(context.Background(), hooks[1].ID, 2)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, hooks[2:], res)
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDocumentHook(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Hook hook.Hook
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
hk := prepDocumentHooks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Hook: hk,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
hk := prepDocumentHooks(t, db, 1, nil)[0]
|
||||
hk.Settings = processor.Settings(`{"url": "http://updated.test"}`)
|
||||
hk.State = processor.State(`{"status":"failing"}`)
|
||||
hk.Score = decimal.NewFromInt(10)
|
||||
hk.UpdatedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
hk.SoftDeletedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
|
||||
return tcase{
|
||||
Hook: hk,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateDocumentHook(ctx, c.Hook)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentHook(context.Background(), c.Hook.ID, c.Hook.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &c.Hook, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocumentHook(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Hook hook.Hook
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
hk := prepDocumentHooks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Hook: hk,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
hk := prepDocumentHooks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Hook: hk,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDocumentHook(ctx, c.Hook.ID, c.Hook.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentHook(context.Background(), c.Hook.ID, c.Hook.OrganizationID)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentHooksByBranchID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentHooksByBranchID(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no hooks
|
||||
res, err = db.FetchDocumentHooksByBranchID(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success - soft-deleted hooks are excluded
|
||||
hooks := prepDocumentHooks(t, db, 2, func(i int, hk *hook.Hook) {
|
||||
if i == 1 {
|
||||
hk.SoftDeletedAt = null.TimeFrom(timeutil.Now().Truncate(time.Second))
|
||||
}
|
||||
})
|
||||
|
||||
res, err = db.FetchDocumentHooksByBranchID(context.Background(), hooks[0].BranchID.V, hooks[0].OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, hooks[:1], res)
|
||||
}
|
||||
|
||||
func Test_agent_SoftDeleteDocumentHooksByBranchID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := db.SoftDeleteDocumentHooksByBranchID(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
|
||||
// success - active hooks are stamped, already deleted ones keep
|
||||
// their original timestamp
|
||||
deletedAt := timeutil.Now().Truncate(time.Second).Add(-time.Hour)
|
||||
|
||||
hooks := prepDocumentHooks(t, db, 2, func(i int, hk *hook.Hook) {
|
||||
if i == 1 {
|
||||
hk.SoftDeletedAt = null.TimeFrom(deletedAt)
|
||||
}
|
||||
})
|
||||
|
||||
err = db.SoftDeleteDocumentHooksByBranchID(context.Background(), hooks[0].BranchID.V, hooks[0].OrganizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var stamps []null.Time
|
||||
|
||||
q, args := db.builder.Select("soft_deleted_at").
|
||||
From("document_hooks").
|
||||
Where(sq.Eq{
|
||||
"id": []xid.ID{hooks[0].ID, hooks[1].ID},
|
||||
}).
|
||||
OrderBy("id ASC").
|
||||
MustSql()
|
||||
|
||||
err = db.sql.Select(&stamps, q, args...)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stamps, 2)
|
||||
|
||||
require.True(t, stamps[0].Valid)
|
||||
assert.WithinDuration(t, timeutil.Now(), stamps[0].Time, time.Minute)
|
||||
assert.True(t, stamps[1].Valid)
|
||||
assert.WithinDuration(t, deletedAt, stamps[1].Time, time.Second)
|
||||
}
|
||||
110
server/core/internal/db/document_maintainers_test.go
Normal file
110
server/core/internal/db/document_maintainers_test.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_agent_UpsertDocumentMaintainers(t *testing.T) {
|
||||
type tcase struct {
|
||||
DocumentID xid.ID
|
||||
OrganizationID string
|
||||
MaintainerIDs []string
|
||||
Result []string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent user": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
DocumentID: doc.ID,
|
||||
OrganizationID: doc.OrganizationID,
|
||||
MaintainerIDs: []string{"non-existent-user-id"},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful upsert": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 2)
|
||||
|
||||
return tcase{
|
||||
DocumentID: doc.ID,
|
||||
OrganizationID: doc.OrganizationID,
|
||||
MaintainerIDs: users,
|
||||
Result: users,
|
||||
}
|
||||
},
|
||||
"Existing maintainers are left untouched": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 2)
|
||||
|
||||
require.NoError(t, db.UpsertDocumentMaintainers(
|
||||
context.Background(),
|
||||
doc.ID,
|
||||
doc.OrganizationID,
|
||||
users[:1],
|
||||
))
|
||||
|
||||
return tcase{
|
||||
DocumentID: doc.ID,
|
||||
OrganizationID: doc.OrganizationID,
|
||||
MaintainerIDs: users,
|
||||
Result: users,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.UpsertDocumentMaintainers(context.Background(), c.DocumentID, c.OrganizationID, c.MaintainerIDs)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchDocumentMaintainers(context.Background(), c.DocumentID, c.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, c.Result, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentMaintainers(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentMaintainers(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no maintainers
|
||||
res, err = db.FetchDocumentMaintainers(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 2)
|
||||
|
||||
require.NoError(t, db.UpsertDocumentMaintainers(context.Background(), doc.ID, doc.OrganizationID, users))
|
||||
|
||||
res, err = db.FetchDocumentMaintainers(context.Background(), doc.ID, doc.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, users, res)
|
||||
}
|
||||
427
server/core/internal/db/document_reviewers_test.go
Normal file
427
server/core/internal/db/document_reviewers_test.go
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepBranchReviewers(t *testing.T, db *DB, count int, fn func(int, *document.BranchReviewer)) []document.BranchReviewer {
|
||||
t.Helper()
|
||||
|
||||
res := make([]document.BranchReviewer, count)
|
||||
|
||||
// reviewers missing a branch after fn ran share one lazily
|
||||
// created branch, so a single call produces co-reviewers.
|
||||
var branch *document.Document
|
||||
|
||||
for i := range count {
|
||||
reviewer := document.BranchReviewer{}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &reviewer)
|
||||
}
|
||||
|
||||
if reviewer.BranchID.IsZero() {
|
||||
if branch == nil {
|
||||
branch = prepDocumentBranches(t, db, 1, nil)[0]
|
||||
}
|
||||
|
||||
reviewer.BranchID = branch.BranchID
|
||||
reviewer.OrganizationID = branch.OrganizationID
|
||||
}
|
||||
|
||||
if reviewer.UserID == "" {
|
||||
reviewer.UserID = prepUsers(t, db, 1)[0]
|
||||
}
|
||||
|
||||
res[i] = reviewer
|
||||
|
||||
q, args := db.builder.Insert("branch_reviewers").
|
||||
SetMap(map[string]any{
|
||||
"fk_branch_id": reviewer.BranchID,
|
||||
"fk_user_id": reviewer.UserID,
|
||||
"fk_organization_id": reviewer.OrganizationID,
|
||||
"currently_approved": reviewer.CurrentlyApproved,
|
||||
"previously_approved": reviewer.PreviouslyApproved,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_FetchBranchReviewers(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchBranchReviewers(ctx, xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no reviewers
|
||||
res, err = db.FetchBranchReviewers(context.Background(), xid.New(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success
|
||||
reviewers := prepBranchReviewers(t, db, 2, nil)
|
||||
|
||||
res, err = db.FetchBranchReviewers(context.Background(), reviewers[0].BranchID, reviewers[0].OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, reviewers, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchBranchReviewer(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchBranchReviewer(context.Background(), xid.New(), "user-id", "org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
reviewer := prepBranchReviewers(t, db, 1, func(_ int, r *document.BranchReviewer) {
|
||||
r.CurrentlyApproved = true
|
||||
})[0]
|
||||
|
||||
res, err = db.FetchBranchReviewer(context.Background(), reviewer.BranchID, reviewer.UserID, reviewer.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &reviewer, res)
|
||||
}
|
||||
|
||||
func Test_agent_InsertBranchReviewer(t *testing.T) {
|
||||
type tcase struct {
|
||||
Reviewer document.BranchReviewer
|
||||
Result document.BranchReviewer
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent branch": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
Reviewer: document.BranchReviewer{
|
||||
BranchID: xid.New(),
|
||||
UserID: users[0],
|
||||
OrganizationID: "non-existent-org-id",
|
||||
},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
branch := prepDocumentBranches(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
reviewer := document.BranchReviewer{
|
||||
BranchID: branch.BranchID,
|
||||
UserID: users[0],
|
||||
OrganizationID: branch.OrganizationID,
|
||||
}
|
||||
|
||||
return tcase{
|
||||
Reviewer: reviewer,
|
||||
Result: reviewer,
|
||||
}
|
||||
},
|
||||
"Existing reviewer is left untouched": func(t *testing.T, db *DB) tcase {
|
||||
original := prepBranchReviewers(t, db, 1, func(_ int, r *document.BranchReviewer) {
|
||||
r.CurrentlyApproved = true
|
||||
})[0]
|
||||
|
||||
reviewer := original
|
||||
reviewer.CurrentlyApproved = false
|
||||
|
||||
return tcase{
|
||||
Reviewer: reviewer,
|
||||
Result: original,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertBranchReviewer(context.Background(), c.Reviewer)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchBranchReviewer(context.Background(), c.Reviewer.BranchID, c.Reviewer.UserID, c.Reviewer.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &c.Result, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UpdateBranchReviewer(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Reviewer document.BranchReviewer
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
reviewer := prepBranchReviewers(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Reviewer: reviewer,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
reviewer := prepBranchReviewers(t, db, 1, nil)[0]
|
||||
reviewer.CurrentlyApproved = true
|
||||
|
||||
return tcase{
|
||||
Reviewer: reviewer,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateBranchReviewer(ctx, c.Reviewer)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchBranchReviewer(context.Background(), c.Reviewer.BranchID, c.Reviewer.UserID, c.Reviewer.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &c.Reviewer, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteBranchReviewer(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Reviewer document.BranchReviewer
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
reviewer := prepBranchReviewers(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Reviewer: reviewer,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
reviewer := prepBranchReviewers(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Reviewer: reviewer,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteBranchReviewer(ctx, c.Reviewer.BranchID, c.Reviewer.UserID, c.Reviewer.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := db.FetchBranchReviewer(context.Background(), c.Reviewer.BranchID, c.Reviewer.UserID, c.Reviewer.OrganizationID)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_PromoteBranchApprovals(t *testing.T) {
|
||||
t.Run("Error returned by the stale reviewer cleanup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("DELETE FROM branch_reviewers").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.PromoteBranchApprovals(context.Background(), xid.New(), xid.New(), "org-id")
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the reviewer update", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("DELETE FROM branch_reviewers").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec("UPDATE branch_reviewers").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.PromoteBranchApprovals(context.Background(), xid.New(), xid.New(), "org-id")
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the reviewer upsert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("DELETE FROM branch_reviewers").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec("UPDATE branch_reviewers").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec("INSERT INTO branch_reviewers").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.PromoteBranchApprovals(context.Background(), xid.New(), xid.New(), "org-id")
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the source cleanup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("DELETE FROM branch_reviewers").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec("UPDATE branch_reviewers").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec("INSERT INTO branch_reviewers").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectExec("DELETE FROM branch_reviewers").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.PromoteBranchApprovals(context.Background(), xid.New(), xid.New(), "org-id")
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("Cancelled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := db.PromoteBranchApprovals(ctx, xid.New(), xid.New(), "org-id")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Successful promotion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
target := prepDocumentBranches(t, db, 1, func(_ int, ndoc *document.Document) {
|
||||
ndoc.ID = doc.ID
|
||||
ndoc.OrganizationID = doc.OrganizationID
|
||||
})[0]
|
||||
users := prepUsers(t, db, 4)
|
||||
|
||||
// the target branch holds one reviewer who never approved
|
||||
// (dropped), one currently approved (demoted to previously
|
||||
// approved and shared with the source branch), and one with
|
||||
// only a stale previous approval.
|
||||
prepBranchReviewers(t, db, 3, func(i int, r *document.BranchReviewer) {
|
||||
r.BranchID = target.BranchID
|
||||
r.OrganizationID = target.OrganizationID
|
||||
r.UserID = users[i]
|
||||
r.CurrentlyApproved = i == 1
|
||||
r.PreviouslyApproved = i == 2
|
||||
})
|
||||
|
||||
// the source branch holds the shared reviewer with a fresh
|
||||
// approval and a brand new reviewer without one.
|
||||
prepBranchReviewers(t, db, 2, func(i int, r *document.BranchReviewer) {
|
||||
r.BranchID = doc.BranchID
|
||||
r.OrganizationID = doc.OrganizationID
|
||||
r.UserID = users[2*i+1]
|
||||
r.CurrentlyApproved = i == 0
|
||||
})
|
||||
|
||||
err := db.PromoteBranchApprovals(context.Background(), doc.BranchID, target.BranchID, doc.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the source branch is cleared.
|
||||
res, err := db.FetchBranchReviewers(context.Background(), doc.BranchID, doc.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// users[0] never approved and is dropped; users[1] is demoted
|
||||
// to previously approved and re-approves via the source branch;
|
||||
// users[2] loses the stale previous approval; users[3] joins
|
||||
// without an approval.
|
||||
res, err = db.FetchBranchReviewers(context.Background(), target.BranchID, target.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, []document.BranchReviewer{
|
||||
{
|
||||
BranchID: target.BranchID,
|
||||
UserID: users[1],
|
||||
OrganizationID: target.OrganizationID,
|
||||
CurrentlyApproved: true,
|
||||
PreviouslyApproved: true,
|
||||
},
|
||||
{
|
||||
BranchID: target.BranchID,
|
||||
UserID: users[2],
|
||||
OrganizationID: target.OrganizationID,
|
||||
CurrentlyApproved: false,
|
||||
PreviouslyApproved: false,
|
||||
},
|
||||
{
|
||||
BranchID: target.BranchID,
|
||||
UserID: users[3],
|
||||
OrganizationID: target.OrganizationID,
|
||||
CurrentlyApproved: false,
|
||||
PreviouslyApproved: false,
|
||||
},
|
||||
}, res)
|
||||
})
|
||||
}
|
||||
216
server/core/internal/db/document_search_job_test.go
Normal file
216
server/core/internal/db/document_search_job_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document/searchgw"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepDocumentSearchJobs(t *testing.T, db *DB, count int, fn func(int, *searchgw.DocumentSearchJob)) []searchgw.DocumentSearchJob {
|
||||
t.Helper()
|
||||
|
||||
res := make([]searchgw.DocumentSearchJob, count)
|
||||
|
||||
for i := range count {
|
||||
job := searchgw.DocumentSearchJob{
|
||||
BlockDiff: searchgw.BlocksDifference{
|
||||
Updated: []searchgw.Block{
|
||||
{
|
||||
ID: "block-" + strconv.Itoa(i),
|
||||
OrganizationID: "org-" + strconv.Itoa(i),
|
||||
DocumentID: xid.New(),
|
||||
Type: "paragraph",
|
||||
Text: "Updated block " + strconv.Itoa(i),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &job)
|
||||
}
|
||||
|
||||
// the id column is a serial; let the database assign it so
|
||||
// the sequence stays usable for production inserts.
|
||||
q, args := db.builder.Insert("document_search_jobs").
|
||||
SetMap(map[string]any{
|
||||
"block_diff": job.BlockDiff,
|
||||
}).
|
||||
Suffix("RETURNING id").
|
||||
MustSql()
|
||||
|
||||
err := db.sql.Get(&job.ID, q, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
res[i] = job
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertDocumentSearchJob(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
BlockDiff searchgw.BlocksDifference
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
BlockDiff: searchgw.BlocksDifference{
|
||||
Added: []searchgw.Block{
|
||||
{
|
||||
ID: "block-added",
|
||||
OrganizationID: "org-added",
|
||||
DocumentID: xid.New(),
|
||||
Type: "heading",
|
||||
Text: "Added block",
|
||||
},
|
||||
},
|
||||
Removed: []searchgw.Block{
|
||||
{
|
||||
ID: "block-removed",
|
||||
OrganizationID: "org-removed",
|
||||
DocumentID: xid.New(),
|
||||
Type: "paragraph",
|
||||
Text: "Removed block",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.InsertDocumentSearchJob(ctx, c.BlockDiff)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var blockDiff searchgw.BlocksDifference
|
||||
|
||||
q, args := db.builder.Select("block_diff").
|
||||
From("document_search_jobs").
|
||||
MustSql()
|
||||
|
||||
err = db.sql.Get(&blockDiff, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.BlockDiff, blockDiff)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentSearchJobs(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentSearchJobs(ctx, 10)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no jobs
|
||||
res, err = db.FetchDocumentSearchJobs(context.Background(), 10)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success - limited batch in id order
|
||||
jobs := prepDocumentSearchJobs(t, db, 3, nil)
|
||||
|
||||
res, err = db.FetchDocumentSearchJobs(context.Background(), 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, jobs[:2], res)
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocumentSearchJob(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
ID int64
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
jobs := prepDocumentSearchJobs(t, db, 1, nil)
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
ID: jobs[0].ID,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
jobs := prepDocumentSearchJobs(t, db, 1, nil)
|
||||
|
||||
return tcase{
|
||||
ID: jobs[0].ID,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteDocumentSearchJob(ctx, c.ID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var id int64
|
||||
|
||||
q, args := db.builder.Select("id").
|
||||
From("document_search_jobs").
|
||||
Where(sq.Eq{
|
||||
"id": c.ID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&id, q, args...)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
|
|
@ -127,11 +128,54 @@ func Test_agent_InsertDocument(t *testing.T) {
|
|||
}
|
||||
|
||||
type tcase struct {
|
||||
Document document.Document
|
||||
Err error
|
||||
CancelledContext bool
|
||||
Document document.Document
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Document: stubDocument(org),
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Sort index race exhausts retries": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
parent := prepDocuments(t, db, 1, func(_ int, ndoc *document.Document) {
|
||||
ndoc.OrganizationID = org
|
||||
})[0]
|
||||
|
||||
// a single child whose sort_index is 1 leaves a gap at 0:
|
||||
// every attempt counts one sibling, picks sort_index 1,
|
||||
// and collides again, so the retry loop runs dry.
|
||||
child := stubDocument(org)
|
||||
child.ParentID = null.ValueFrom(parent.ID)
|
||||
|
||||
q, args := db.builder.Insert("documents").
|
||||
SetMap(map[string]any{
|
||||
"id": child.ID,
|
||||
"sort_index": 1,
|
||||
"fk_organization_id": org,
|
||||
"fk_parent_id": child.ParentID,
|
||||
"created_at": child.CreatedAt,
|
||||
"updated_at": child.UpdatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
doc := stubDocument(org)
|
||||
doc.ParentID = null.ValueFrom(parent.ID)
|
||||
|
||||
return tcase{
|
||||
Document: doc,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Duplicate key": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
doc := prepDocuments(t, db, 1, func(_ int, ndoc *document.Document) {
|
||||
|
|
@ -159,7 +203,14 @@ func Test_agent_InsertDocument(t *testing.T) {
|
|||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertDocument(context.Background(), c.Document)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.InsertDocument(ctx, c.Document)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -181,6 +232,20 @@ func Test_agent_InsertDocument(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_agent_CheckDocumentExists(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
err := db.CheckDocumentExists(context.Background(), xid.New(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
|
||||
// success
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
err = db.CheckDocumentExists(context.Background(), doc.ID, doc.OrganizationID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocument(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
|
|
@ -198,6 +263,20 @@ func Test_agent_FetchDocument(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_agent_UpdateDocumentTree(t *testing.T) {
|
||||
// error - failing update statement inside the transaction
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE documents").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.UpdateDocumentTree(
|
||||
context.Background(),
|
||||
document.Summaries{{ID: xid.New()}},
|
||||
"org-id",
|
||||
)
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
|
@ -235,7 +314,7 @@ func Test_agent_UpdateDocumentTree(t *testing.T) {
|
|||
// success swapping root elements
|
||||
exp1[0], exp1[1] = exp1[1], exp1[0] // swap root elements
|
||||
|
||||
err := db.UpdateDocumentTree(context.Background(), exp1, org)
|
||||
err = db.UpdateDocumentTree(context.Background(), exp1, org)
|
||||
assert.NoError(t, err)
|
||||
|
||||
exp2 := exp1[0].Children
|
||||
|
|
@ -310,7 +389,93 @@ func Test_agent_FetchDocumentTree(t *testing.T) {
|
|||
assert.Equal(t, exp1, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchDocumentTreeByDocumentParentID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchDocumentTreeByDocumentParentID(ctx, null.Value[xid.ID]{}, "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no documents
|
||||
res, err = db.FetchDocumentTreeByDocumentParentID(context.Background(), null.Value[xid.ID]{}, "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
roots := prepDocuments(t, db, 2, func(_ int, ndoc *document.Document) {
|
||||
ndoc.OrganizationID = org
|
||||
})
|
||||
|
||||
exp := make(document.Summaries, 0, len(roots))
|
||||
|
||||
for _, doc := range roots {
|
||||
exp = append(exp, document.Summary{
|
||||
ID: doc.ID,
|
||||
DocumentName: doc.DocumentName,
|
||||
Icon: doc.Icon,
|
||||
Protected: doc.Protected,
|
||||
})
|
||||
}
|
||||
|
||||
children := prepDocuments(t, db, 2, func(_ int, sdoc *document.Document) {
|
||||
sdoc.OrganizationID = org
|
||||
sdoc.ParentID = null.ValueFrom(roots[0].ID)
|
||||
})
|
||||
|
||||
// success - root documents only, children excluded
|
||||
res, err = db.FetchDocumentTreeByDocumentParentID(context.Background(), null.Value[xid.ID]{}, org)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, exp, res)
|
||||
|
||||
exp = exp[:0]
|
||||
|
||||
for _, doc := range children {
|
||||
exp = append(exp, document.Summary{
|
||||
ID: doc.ID,
|
||||
DocumentName: doc.DocumentName,
|
||||
Icon: doc.Icon,
|
||||
Protected: doc.Protected,
|
||||
})
|
||||
}
|
||||
|
||||
// success - direct children of the first root
|
||||
res, err = db.FetchDocumentTreeByDocumentParentID(context.Background(), null.ValueFrom(roots[0].ID), org)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, exp, res)
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDocument(t *testing.T) {
|
||||
t.Run("Error returned by the document update", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE documents").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.UpdateDocument(context.Background(), document.Document{})
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the branch upsert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE documents").WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("INSERT INTO document_branches").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.UpdateDocument(context.Background(), document.Document{})
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
type tcase struct {
|
||||
Document document.Document
|
||||
Err error
|
||||
|
|
@ -372,6 +537,91 @@ func Test_agent_UpdateDocument(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_agent_UpdateDocumentParentID(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
ID xid.ID
|
||||
ParentID null.Value[xid.ID]
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
doc := prepDocuments(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
ID: doc.ID,
|
||||
OrganizationID: doc.OrganizationID,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update to a new parent": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
docs := prepDocuments(t, db, 2, func(_ int, ndoc *document.Document) {
|
||||
ndoc.OrganizationID = org
|
||||
})
|
||||
|
||||
return tcase{
|
||||
ID: docs[1].ID,
|
||||
ParentID: null.ValueFrom(docs[0].ID),
|
||||
OrganizationID: org,
|
||||
}
|
||||
},
|
||||
"Successful update to a root document": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
parent := prepDocuments(t, db, 1, func(_ int, ndoc *document.Document) {
|
||||
ndoc.OrganizationID = org
|
||||
})[0]
|
||||
child := prepDocuments(t, db, 1, func(_ int, ndoc *document.Document) {
|
||||
ndoc.OrganizationID = org
|
||||
ndoc.ParentID = null.ValueFrom(parent.ID)
|
||||
})[0]
|
||||
|
||||
return tcase{
|
||||
ID: child.ID,
|
||||
OrganizationID: org,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateDocumentParentID(ctx, c.ID, c.ParentID, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var parentID null.Value[xid.ID]
|
||||
|
||||
q, args := db.builder.Select("fk_parent_id").
|
||||
From("documents").
|
||||
Where(sq.Eq{
|
||||
"id": c.ID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&parentID, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.ParentID, parentID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteDocument(t *testing.T) {
|
||||
type tcase struct {
|
||||
ID xid.ID
|
||||
|
|
|
|||
327
server/core/internal/db/github_installation_test.go
Normal file
327
server/core/internal/db/github_installation_test.go
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepGithubInstallations(t *testing.T, db *DB, count int, organizationID string) []int64 { //nolint:unparam // fixture helpers share a uniform signature
|
||||
t.Helper()
|
||||
|
||||
res := make([]int64, count)
|
||||
|
||||
for i := range count {
|
||||
id := int64(i + 1)
|
||||
|
||||
res[i] = id
|
||||
|
||||
vals := map[string]any{
|
||||
"installation_id": id,
|
||||
}
|
||||
|
||||
if organizationID != "" {
|
||||
vals["fk_organization_id"] = organizationID
|
||||
}
|
||||
|
||||
q, args := db.builder.Insert("github_installations").
|
||||
SetMap(vals).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertGithubInstallation(t *testing.T) {
|
||||
type tcase struct {
|
||||
InstallationID int64
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Duplicate installation ID": func(t *testing.T, db *DB) tcase {
|
||||
ids := prepGithubInstallations(t, db, 1, "")
|
||||
|
||||
return tcase{
|
||||
InstallationID: ids[0],
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
InstallationID: 123,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertGithubInstallation(context.Background(), c.InstallationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var installationID int64
|
||||
|
||||
q, args := db.builder.Select("installation_id").
|
||||
From("github_installations").
|
||||
Where(sq.Eq{
|
||||
"installation_id": c.InstallationID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&installationID, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.InstallationID, installationID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchGithubInstallationByOrganizationID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchGithubInstallationByOrganizationID(context.Background(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Zero(t, res)
|
||||
|
||||
// success
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
ids := prepGithubInstallations(t, db, 1, org)
|
||||
|
||||
res, err = db.FetchGithubInstallationByOrganizationID(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ids[0], res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchGithubInstallationOrganizationID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchGithubInstallationOrganizationID(context.Background(), 404)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Zero(t, res)
|
||||
|
||||
// success - unassigned installation
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
ids := prepGithubInstallations(t, db, 1, "")
|
||||
|
||||
res, err = db.FetchGithubInstallationOrganizationID(context.Background(), ids[0])
|
||||
require.NoError(t, err)
|
||||
assert.False(t, res.Valid)
|
||||
|
||||
// success - assigned installation
|
||||
q, args := db.builder.Update("github_installations").
|
||||
SetMap(map[string]any{
|
||||
"fk_organization_id": org,
|
||||
}).
|
||||
Where(sq.Eq{
|
||||
"installation_id": ids[0],
|
||||
}).MustSql()
|
||||
|
||||
_, err = db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err = db.FetchGithubInstallationOrganizationID(context.Background(), ids[0])
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(org), res)
|
||||
}
|
||||
|
||||
func Test_agent_UpdateGithubInstallationOrganizationID(t *testing.T) {
|
||||
type tcase struct {
|
||||
InstallationID int64
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent organization": func(t *testing.T, db *DB) tcase {
|
||||
ids := prepGithubInstallations(t, db, 1, "")
|
||||
|
||||
return tcase{
|
||||
InstallationID: ids[0],
|
||||
OrganizationID: "non-existent-org-id",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
ids := prepGithubInstallations(t, db, 1, "")
|
||||
|
||||
return tcase{
|
||||
InstallationID: ids[0],
|
||||
OrganizationID: org,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.UpdateGithubInstallationOrganizationID(context.Background(), c.InstallationID, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var organizationID null.String
|
||||
|
||||
q, args := db.builder.Select("fk_organization_id").
|
||||
From("github_installations").
|
||||
Where(sq.Eq{
|
||||
"installation_id": c.InstallationID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&organizationID, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(c.OrganizationID), organizationID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_DeleteGithubInstallation(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
InstallationID int64
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
ids := prepGithubInstallations(t, db, 1, "")
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
InstallationID: ids[0],
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
ids := prepGithubInstallations(t, db, 1, "")
|
||||
|
||||
return tcase{
|
||||
InstallationID: ids[0],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteGithubInstallation(ctx, c.InstallationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var installationID int64
|
||||
|
||||
q, args := db.builder.Select("installation_id").
|
||||
From("github_installations").
|
||||
Where(sq.Eq{
|
||||
"installation_id": c.InstallationID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&installationID, q, args...)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UnassignGithubInstallationOrganization(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
InstallationID int64
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
ids := prepGithubInstallations(t, db, 1, org)
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
InstallationID: ids[0],
|
||||
OrganizationID: org,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful unassign": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
ids := prepGithubInstallations(t, db, 1, org)
|
||||
|
||||
return tcase{
|
||||
InstallationID: ids[0],
|
||||
OrganizationID: org,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UnassignGithubInstallationOrganization(ctx, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var organizationID null.String
|
||||
|
||||
q, args := db.builder.Select("fk_organization_id").
|
||||
From("github_installations").
|
||||
Where(sq.Eq{
|
||||
"installation_id": c.InstallationID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&organizationID, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, organizationID.Valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,13 @@ func (a *agent) CreateNotification(ctx context.Context, nt *notification.Notific
|
|||
return err
|
||||
}
|
||||
|
||||
// a zero limit means unlimited retention; without this guard
|
||||
// the subquery below would emit LIMIT 0 and the delete would
|
||||
// drop every notification of the user.
|
||||
if a.opts.MaxNotifications == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
b := a.builder.Select("id").
|
||||
From("notifications").
|
||||
Where(sq.Eq{
|
||||
|
|
|
|||
454
server/core/internal/db/notification_test.go
Normal file
454
server/core/internal/db/notification_test.go
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/oxynote/oxynote/server/core/internal/notification"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/httpserver"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/sqlutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepNotifications(t *testing.T, db *DB, count int, fn func(int, *notification.Notification)) []*notification.Notification {
|
||||
t.Helper()
|
||||
|
||||
res := make([]*notification.Notification, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
// notifications missing an owner after fn ran share one lazily
|
||||
// created organization member.
|
||||
var (
|
||||
organizationID string
|
||||
userID string
|
||||
)
|
||||
|
||||
for i := range count {
|
||||
nt := ¬ification.Notification{
|
||||
Core: notification.Core{
|
||||
Code: "test.notification",
|
||||
Metadata: notification.Metadata{
|
||||
"index": strconv.Itoa(i),
|
||||
},
|
||||
},
|
||||
ID: xid.New(),
|
||||
// distinct ascending timestamps keep the fetch order
|
||||
// deterministic.
|
||||
CreatedAt: now.Add(time.Duration(i) * time.Second),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, nt)
|
||||
}
|
||||
|
||||
if nt.UserID == "" {
|
||||
if userID == "" {
|
||||
organizationID = prepOrganizations(t, db, 1)[0]
|
||||
userID = prepUsers(t, db, 1)[0]
|
||||
}
|
||||
|
||||
nt.UserID = userID
|
||||
nt.OrganizationID = organizationID
|
||||
}
|
||||
|
||||
res[i] = nt
|
||||
|
||||
q, args := db.builder.Insert("notifications").
|
||||
SetMap(map[string]any{
|
||||
"id": nt.ID,
|
||||
"code": nt.Code,
|
||||
"metadata": nt.Metadata,
|
||||
"fk_user_id": nt.UserID,
|
||||
"fk_organization_id": nt.OrganizationID,
|
||||
"read": nt.Read,
|
||||
"created_at": nt.CreatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_CreateNotification(t *testing.T) {
|
||||
stubNotification := func(organizationID, userID string) *notification.Notification {
|
||||
return ¬ification.Notification{
|
||||
Core: notification.Core{
|
||||
Code: "test.notification",
|
||||
Metadata: notification.Metadata{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
ID: xid.New(),
|
||||
UserID: userID,
|
||||
OrganizationID: organizationID,
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("Cancelled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := db.CreateNotification(ctx, stubNotification("org-id", "user-id"))
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the insert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
nt := prepNotifications(t, db, 1, nil)[0]
|
||||
|
||||
err := db.CreateNotification(context.Background(), nt)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Error returned by the retention delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, mock := prepMockDB(t)
|
||||
a.opts.MaxNotifications = 1
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("INSERT INTO notifications").WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("DELETE FROM notifications").WillReturnError(assert.AnError)
|
||||
mock.ExpectRollback()
|
||||
|
||||
err := a.CreateNotification(context.Background(), stubNotification("org-id", "user-id"))
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("Successful creation without a retention limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
|
||||
existing := prepNotifications(t, db, 2, nil)
|
||||
nt := stubNotification(existing[0].OrganizationID, existing[0].UserID)
|
||||
|
||||
err := db.CreateNotification(context.Background(), nt)
|
||||
require.NoError(t, err)
|
||||
|
||||
count, err := db.FetchNotificationCount(context.Background(), nt.OrganizationID, nt.UserID, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(3), count)
|
||||
})
|
||||
|
||||
t.Run("Successful creation with retention trimming", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
db.opts.MaxNotifications = 2
|
||||
|
||||
existing := prepNotifications(t, db, 2, nil)
|
||||
nt := stubNotification(existing[0].OrganizationID, existing[0].UserID)
|
||||
nt.CreatedAt = existing[1].CreatedAt.Add(time.Hour)
|
||||
|
||||
err := db.CreateNotification(context.Background(), nt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// the oldest notification is trimmed; the two newest remain.
|
||||
res, _, err := db.FetchManyNotifications(
|
||||
context.Background(),
|
||||
nt.OrganizationID,
|
||||
nt.UserID,
|
||||
httpserver.Query{Limit: 10, Page: 1},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, []*notification.Notification{nt, existing[1]}, res)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_agent_FetchManyNotifications(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - invalid query
|
||||
res, pages, err := db.FetchManyNotifications(context.Background(), "org-id", "user-id", httpserver.Query{})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
assert.Zero(t, pages)
|
||||
|
||||
// error - invalid filter key
|
||||
res, pages, err = db.FetchManyNotifications(context.Background(), "org-id", "user-id", httpserver.Query{
|
||||
Limit: 10,
|
||||
Page: 1,
|
||||
Filters: map[string]string{
|
||||
"unknown": "true",
|
||||
},
|
||||
})
|
||||
assert.Equal(t, httpserver.ErrInvalidFilterKey, err)
|
||||
assert.Nil(t, res)
|
||||
assert.Zero(t, pages)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, pages, err = db.FetchManyNotifications(ctx, "org-id", "user-id", httpserver.Query{Limit: 10, Page: 1})
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
assert.Zero(t, pages)
|
||||
|
||||
// success - no notifications
|
||||
res, pages, err = db.FetchManyNotifications(context.Background(), "org-id", "user-id", httpserver.Query{Limit: 10, Page: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
assert.Zero(t, pages)
|
||||
|
||||
notifications := prepNotifications(t, db, 3, func(i int, nt *notification.Notification) {
|
||||
nt.Read = i == 0
|
||||
})
|
||||
|
||||
organizationID := notifications[0].OrganizationID
|
||||
userID := notifications[0].UserID
|
||||
|
||||
// success - first page, newest first
|
||||
res, pages, err = db.FetchManyNotifications(context.Background(), organizationID, userID, httpserver.Query{
|
||||
Limit: 2,
|
||||
Page: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(2), pages)
|
||||
testutil.AssertFilterEqual(t, []*notification.Notification{notifications[2], notifications[1]}, res)
|
||||
|
||||
// success - second page
|
||||
res, pages, err = db.FetchManyNotifications(context.Background(), organizationID, userID, httpserver.Query{
|
||||
Limit: 2,
|
||||
Page: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(2), pages)
|
||||
testutil.AssertFilterEqual(t, []*notification.Notification{notifications[0]}, res)
|
||||
|
||||
// success - filtered by read state
|
||||
res, pages, err = db.FetchManyNotifications(context.Background(), organizationID, userID, httpserver.Query{
|
||||
Limit: 10,
|
||||
Page: 1,
|
||||
Filters: map[string]string{
|
||||
notification.FilterReadEq: "true",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), pages)
|
||||
testutil.AssertFilterEqual(t, []*notification.Notification{notifications[0]}, res)
|
||||
|
||||
// success - sorted by creation time ascending
|
||||
res, pages, err = db.FetchManyNotifications(context.Background(), organizationID, userID, httpserver.Query{
|
||||
Limit: 10,
|
||||
Page: 1,
|
||||
SortKeys: []httpserver.SortKey{
|
||||
{Key: notification.SortCreatedAt, Asc: true},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), pages)
|
||||
testutil.AssertFilterEqual(t, notifications, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchNotificationCount(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
count, err := db.FetchNotificationCount(ctx, "org-id", "user-id", false)
|
||||
require.Error(t, err)
|
||||
assert.Zero(t, count)
|
||||
|
||||
// success
|
||||
notifications := prepNotifications(t, db, 3, func(i int, nt *notification.Notification) {
|
||||
nt.Read = i == 0
|
||||
})
|
||||
|
||||
count, err = db.FetchNotificationCount(context.Background(), notifications[0].OrganizationID, notifications[0].UserID, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(2), count)
|
||||
|
||||
count, err = db.FetchNotificationCount(context.Background(), notifications[0].OrganizationID, notifications[0].UserID, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(1), count)
|
||||
}
|
||||
|
||||
func Test_agent_MarkReadByNotificationsIDs(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
OrganizationID string
|
||||
UserID string
|
||||
IDs []xid.ID
|
||||
UnreadCount uint64
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
nt := prepNotifications(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
OrganizationID: nt.OrganizationID,
|
||||
UserID: nt.UserID,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update of specific notifications": func(t *testing.T, db *DB) tcase {
|
||||
notifications := prepNotifications(t, db, 3, nil)
|
||||
|
||||
return tcase{
|
||||
OrganizationID: notifications[0].OrganizationID,
|
||||
UserID: notifications[0].UserID,
|
||||
IDs: []xid.ID{notifications[0].ID, notifications[1].ID},
|
||||
UnreadCount: 1,
|
||||
}
|
||||
},
|
||||
"Successful update of all notifications": func(t *testing.T, db *DB) tcase {
|
||||
notifications := prepNotifications(t, db, 3, nil)
|
||||
|
||||
return tcase{
|
||||
OrganizationID: notifications[0].OrganizationID,
|
||||
UserID: notifications[0].UserID,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.MarkReadByNotificationsIDs(ctx, c.OrganizationID, c.UserID, c.IDs)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
count, err := db.FetchNotificationCount(context.Background(), c.OrganizationID, c.UserID, false)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.UnreadCount, count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_applyNotificationFilter(t *testing.T) {
|
||||
cc := map[string]struct {
|
||||
Key string
|
||||
Value string
|
||||
Result sq.SelectBuilder
|
||||
Err error
|
||||
}{
|
||||
"Invalid key": {
|
||||
Key: "unknown",
|
||||
Err: httpserver.ErrInvalidFilterKey,
|
||||
},
|
||||
"Invalid read value": {
|
||||
Key: notification.FilterReadEq,
|
||||
Value: "not-a-bool",
|
||||
Err: httpserver.ErrInvalidFilterValue,
|
||||
},
|
||||
"Successful execution with empty key": {
|
||||
Result: sq.Select(),
|
||||
},
|
||||
"Successful execution with read key": {
|
||||
Key: notification.FilterReadEq,
|
||||
Value: "true",
|
||||
Result: sq.Select().Where(sq.Eq{
|
||||
"read": true,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b, err := applyNotificationFilter(sq.Select(), c.Key, c.Value)
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, c.Result, b)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_applyNotificationSort(t *testing.T) {
|
||||
cc := map[string]struct {
|
||||
Key httpserver.SortKey
|
||||
Result sq.SelectBuilder
|
||||
Err error
|
||||
}{
|
||||
"Invalid key": {
|
||||
Key: httpserver.SortKey{
|
||||
Key: "unknown",
|
||||
Asc: true,
|
||||
},
|
||||
Err: httpserver.ErrInvalidSortKey,
|
||||
},
|
||||
"Successful execution with empty key": {
|
||||
Key: httpserver.SortKey{
|
||||
Asc: true,
|
||||
},
|
||||
Result: sq.Select().OrderBy(sqlutil.SortString(
|
||||
httpserver.SortKey{
|
||||
Key: "notifications." + notification.SortCreatedAt,
|
||||
Asc: false,
|
||||
},
|
||||
)),
|
||||
},
|
||||
"Successful execution with created_at key": {
|
||||
Key: httpserver.SortKey{
|
||||
Key: notification.SortCreatedAt,
|
||||
Asc: true,
|
||||
},
|
||||
Result: sq.Select().OrderBy(sqlutil.SortString(
|
||||
httpserver.SortKey{
|
||||
Key: "notifications." + notification.SortCreatedAt,
|
||||
Asc: true,
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b, err := applyNotificationSort(sq.Select(), c.Key)
|
||||
testutil.AssertEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, c.Result, b)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -20,10 +25,12 @@ func prepOrganizations(t *testing.T, db *DB, count int) []string { //nolint:unpa
|
|||
|
||||
res[i] = id
|
||||
|
||||
// the slug embeds the random ID so repeated fixture calls
|
||||
// within one database never collide on the unique constraint.
|
||||
q, args := db.builder.Insert("organizations").Values(
|
||||
id,
|
||||
"Organization "+strconv.Itoa(i),
|
||||
"org-"+strconv.Itoa(i),
|
||||
"org-"+id,
|
||||
"",
|
||||
timeutil.Now().Truncate(time.Second),
|
||||
"",
|
||||
|
|
@ -35,3 +42,158 @@ func prepOrganizations(t *testing.T, db *DB, count int) []string { //nolint:unpa
|
|||
|
||||
return res
|
||||
}
|
||||
|
||||
func prepOrganizationMembers(t *testing.T, db *DB, organizationID string, userIDs []string) {
|
||||
t.Helper()
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
for i, userID := range userIDs {
|
||||
q, args := db.builder.Insert("organization_members").
|
||||
SetMap(map[string]any{
|
||||
"id": xid.New().String(),
|
||||
"fk_organization_id": organizationID,
|
||||
"fk_user_id": userID,
|
||||
"role": "member",
|
||||
// distinct timestamps keep the fetch order deterministic.
|
||||
"created_at": now.Add(time.Duration(i) * time.Second),
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchOrganizationMembers(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchOrganizationMembers(ctx, "org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no members
|
||||
res, err = db.FetchOrganizationMembers(context.Background(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
users := prepUsers(t, db, 3)
|
||||
prepOrganizationMembers(t, db, org, users)
|
||||
|
||||
res, err = db.FetchOrganizationMembers(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, users, res)
|
||||
}
|
||||
|
||||
func Test_agent_CheckOrganizationMember(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
ok, err := db.CheckOrganizationMember(ctx, org, users[0])
|
||||
require.Error(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
// success - not a member
|
||||
ok, err = db.CheckOrganizationMember(context.Background(), org, users[0])
|
||||
require.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
// success - member
|
||||
prepOrganizationMembers(t, db, org, users)
|
||||
|
||||
ok, err = db.CheckOrganizationMember(context.Background(), org, users[0])
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func Test_agent_FetchOrganizationSlug(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
slug, err := db.FetchOrganizationSlug(context.Background(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Empty(t, slug)
|
||||
|
||||
// success
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
slug, err = db.FetchOrganizationSlug(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "org-"+org, slug)
|
||||
}
|
||||
|
||||
func Test_agent_UpdateOrganizationLogo(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
OrganizationID string
|
||||
Logo string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
OrganizationID: org,
|
||||
Logo: "new-logo",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
OrganizationID: org,
|
||||
Logo: "new-logo",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateOrganizationLogo(ctx, c.OrganizationID, c.Logo)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var logo string
|
||||
|
||||
q, args := db.builder.Select("logo").
|
||||
From("organizations").
|
||||
Where(sq.Eq{
|
||||
"id": c.OrganizationID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&logo, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.Logo, logo)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
465
server/core/internal/db/slack_app_test.go
Normal file
465
server/core/internal/db/slack_app_test.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/apps/slackapp"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepSlackApps(t *testing.T, db *DB, count int, fn func(int, *slackapp.App)) []slackapp.App { //nolint:unparam // fixture helpers share a uniform signature
|
||||
t.Helper()
|
||||
|
||||
res := make([]slackapp.App, count)
|
||||
|
||||
for i := range count {
|
||||
app := slackapp.App{
|
||||
// the team ID embeds a random ID so repeated fixture
|
||||
// calls within one database never collide.
|
||||
TeamID: "team-" + xid.New().String(),
|
||||
Token: "token-" + strconv.Itoa(i),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &app)
|
||||
}
|
||||
|
||||
res[i] = app
|
||||
|
||||
q, args := db.builder.Insert("slack_apps").
|
||||
SetMap(map[string]any{
|
||||
"team_id": app.TeamID,
|
||||
"fk_organization_id": app.OrganizationID,
|
||||
"token": app.Token,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func prepSlackMessages(t *testing.T, db *DB, count int, fn func(int, *slackapp.Message)) []slackapp.Message {
|
||||
t.Helper()
|
||||
|
||||
res := make([]slackapp.Message, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
for i := range count {
|
||||
msg := slackapp.Message{
|
||||
ID: xid.New(),
|
||||
Text: "Message " + strconv.Itoa(i),
|
||||
// distinct timestamps keep the fetch order deterministic.
|
||||
CreatedAt: now.Add(-time.Duration(i) * time.Second),
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &msg)
|
||||
}
|
||||
|
||||
if msg.OrganizationID == "" {
|
||||
msg.OrganizationID = prepOrganizations(t, db, 1)[0]
|
||||
}
|
||||
|
||||
res[i] = msg
|
||||
|
||||
q, args := db.builder.Insert("slack_messages").
|
||||
SetMap(map[string]any{
|
||||
"id": msg.ID,
|
||||
"fk_organization_id": msg.OrganizationID,
|
||||
"text": msg.Text,
|
||||
"created_at": msg.CreatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertSlackApp(t *testing.T) {
|
||||
type tcase struct {
|
||||
App slackapp.App
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent organization": func(_ *testing.T, _ *DB) tcase {
|
||||
return tcase{
|
||||
App: slackapp.App{
|
||||
TeamID: "team-1",
|
||||
OrganizationID: null.StringFrom("non-existent-org-id"),
|
||||
Token: "token-1",
|
||||
},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
App: slackapp.App{
|
||||
TeamID: "team-1",
|
||||
OrganizationID: null.StringFrom(org),
|
||||
Token: "token-1",
|
||||
},
|
||||
}
|
||||
},
|
||||
"Successful upsert of an existing team": func(t *testing.T, db *DB) tcase {
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
app.Token = "rotated-token"
|
||||
|
||||
return tcase{
|
||||
App: app,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertSlackApp(context.Background(), c.App)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var app slackapp.App
|
||||
|
||||
q, args := db.selectSlackApp(db.builder.Select()).
|
||||
Where(sq.Eq{
|
||||
"team_id": c.App.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&app, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.App, app)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_InsertSlackMessage(t *testing.T) {
|
||||
type tcase struct {
|
||||
Message slackapp.Message
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Duplicate message ID": func(t *testing.T, db *DB) tcase {
|
||||
msg := prepSlackMessages(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Message: msg,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
|
||||
return tcase{
|
||||
Message: slackapp.Message{
|
||||
ID: xid.New(),
|
||||
OrganizationID: org,
|
||||
Text: "Hello from Slack",
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertSlackMessage(context.Background(), c.Message)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var msg slackapp.Message
|
||||
|
||||
q, args := db.selectSlackMessage(db.builder.Select()).
|
||||
Where(sq.Eq{
|
||||
"id": c.Message.ID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&msg, q, args...)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, c.Message, msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchSlackMessages(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - cancelled context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
res, err := db.FetchSlackMessages(ctx, "non-existent-org-id")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success - no messages
|
||||
res, err = db.FetchSlackMessages(context.Background(), "non-existent-org-id")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, res)
|
||||
|
||||
// success - ordered by creation time, newest first
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
messages := prepSlackMessages(t, db, 3, func(_ int, msg *slackapp.Message) {
|
||||
msg.OrganizationID = org
|
||||
})
|
||||
|
||||
res, err = db.FetchSlackMessages(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, messages, res)
|
||||
}
|
||||
|
||||
func Test_agent_UpdateSlackAppOrganizationID(t *testing.T) {
|
||||
type tcase struct {
|
||||
TeamID string
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent organization": func(t *testing.T, db *DB) tcase {
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
TeamID: app.TeamID,
|
||||
OrganizationID: "non-existent-org-id",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
TeamID: app.TeamID,
|
||||
OrganizationID: org,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.UpdateSlackAppOrganizationID(context.Background(), c.TeamID, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var organizationID null.String
|
||||
|
||||
q, args := db.builder.Select("fk_organization_id").
|
||||
From("slack_apps").
|
||||
Where(sq.Eq{
|
||||
"team_id": c.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&organizationID, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, null.StringFrom(c.OrganizationID), organizationID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchSlackAppByTeamID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchSlackAppByTeamID(context.Background(), "non-existent-team-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchSlackAppByTeamID(context.Background(), app.TeamID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &app, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchSlackAppByOrganizationID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchSlackAppByOrganizationID(context.Background(), "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
app := prepSlackApps(t, db, 1, func(_ int, app *slackapp.App) {
|
||||
app.OrganizationID = null.StringFrom(org)
|
||||
})[0]
|
||||
|
||||
res, err = db.FetchSlackAppByOrganizationID(context.Background(), org)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &app, res)
|
||||
}
|
||||
|
||||
func Test_agent_DeleteSlackApp(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
TeamID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
TeamID: app.TeamID,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
TeamID: app.TeamID,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteSlackApp(ctx, c.TeamID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var teamID string
|
||||
|
||||
q, args := db.builder.Select("team_id").
|
||||
From("slack_apps").
|
||||
Where(sq.Eq{
|
||||
"team_id": c.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&teamID, q, args...)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UnassignSlackAppOrganization(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
TeamID string
|
||||
OrganizationID string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
app := prepSlackApps(t, db, 1, func(_ int, app *slackapp.App) {
|
||||
app.OrganizationID = null.StringFrom(org)
|
||||
})[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
TeamID: app.TeamID,
|
||||
OrganizationID: org,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful unassign": func(t *testing.T, db *DB) tcase {
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
app := prepSlackApps(t, db, 1, func(_ int, app *slackapp.App) {
|
||||
app.OrganizationID = null.StringFrom(org)
|
||||
})[0]
|
||||
|
||||
return tcase{
|
||||
TeamID: app.TeamID,
|
||||
OrganizationID: org,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UnassignSlackAppOrganization(ctx, c.OrganizationID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var organizationID null.String
|
||||
|
||||
q, args := db.builder.Select("fk_organization_id").
|
||||
From("slack_apps").
|
||||
Where(sq.Eq{
|
||||
"team_id": c.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&organizationID, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, organizationID.Valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
293
server/core/internal/db/slack_user_link_test.go
Normal file
293
server/core/internal/db/slack_user_link_test.go
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/apps/slackapp"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepSlackUserLinks(t *testing.T, db *DB, count int, fn func(int, *slackapp.UserLink)) []slackapp.UserLink { //nolint:unparam // fixture helpers share a uniform signature
|
||||
t.Helper()
|
||||
|
||||
res := make([]slackapp.UserLink, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
for i := range count {
|
||||
link := slackapp.UserLink{
|
||||
SlackUserID: "slack-user-" + strconv.Itoa(i),
|
||||
Settings: slackapp.UserLinkSettings{
|
||||
Notifications: true,
|
||||
},
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(i, &link)
|
||||
}
|
||||
|
||||
if link.TeamID == "" {
|
||||
link.TeamID = prepSlackApps(t, db, 1, nil)[0].TeamID
|
||||
}
|
||||
|
||||
if link.UserID == "" {
|
||||
link.UserID = prepUsers(t, db, 1)[0]
|
||||
}
|
||||
|
||||
res[i] = link
|
||||
|
||||
q, args := db.builder.Insert("slack_user_links").
|
||||
SetMap(map[string]any{
|
||||
"slack_user_id": link.SlackUserID,
|
||||
"fk_team_id": link.TeamID,
|
||||
"fk_user_id": link.UserID,
|
||||
"settings": link.Settings,
|
||||
"created_at": link.CreatedAt,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_InsertSlackUserLink(t *testing.T) {
|
||||
type tcase struct {
|
||||
Link slackapp.UserLink
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Non-existent team": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
Link: slackapp.UserLink{
|
||||
SlackUserID: "slack-user-1",
|
||||
TeamID: "non-existent-team-id",
|
||||
UserID: users[0],
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful insert": func(t *testing.T, db *DB) tcase {
|
||||
app := prepSlackApps(t, db, 1, nil)[0]
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
Link: slackapp.UserLink{
|
||||
SlackUserID: "slack-user-1",
|
||||
TeamID: app.TeamID,
|
||||
UserID: users[0],
|
||||
Settings: slackapp.UserLinkSettings{
|
||||
Notifications: true,
|
||||
},
|
||||
CreatedAt: timeutil.Now().Truncate(time.Second),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
err := db.InsertSlackUserLink(context.Background(), c.Link)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var link slackapp.UserLink
|
||||
|
||||
q, args := db.selectSlackUserLink(db.builder.Select()).
|
||||
Where(sq.Eq{
|
||||
"slack_user_id": c.Link.SlackUserID,
|
||||
"fk_team_id": c.Link.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&link, q, args...)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, c.Link, link)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_UpdateSlackUserLink(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Link slackapp.UserLink
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
link := prepSlackUserLinks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Link: link,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
link := prepSlackUserLinks(t, db, 1, nil)[0]
|
||||
link.Settings.Notifications = false
|
||||
|
||||
return tcase{
|
||||
Link: link,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateSlackUserLink(ctx, c.Link)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var link slackapp.UserLink
|
||||
|
||||
q, args := db.selectSlackUserLink(db.builder.Select()).
|
||||
Where(sq.Eq{
|
||||
"slack_user_id": c.Link.SlackUserID,
|
||||
"fk_team_id": c.Link.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&link, q, args...)
|
||||
require.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, c.Link, link)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchSlackUserLink(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchSlackUserLink(context.Background(), "non-existent-slack-user-id", "non-existent-team-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
link := prepSlackUserLinks(t, db, 1, nil)[0]
|
||||
|
||||
res, err = db.FetchSlackUserLink(context.Background(), link.SlackUserID, link.TeamID)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &link, res)
|
||||
}
|
||||
|
||||
func Test_agent_FetchSlackUserLinkByUserID(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
res, err := db.FetchSlackUserLinkByUserID(context.Background(), "non-existent-user-id", "non-existent-org-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Nil(t, res)
|
||||
|
||||
// success
|
||||
org := prepOrganizations(t, db, 1)[0]
|
||||
app := prepSlackApps(t, db, 1, func(_ int, app *slackapp.App) {
|
||||
app.OrganizationID = null.StringFrom(org)
|
||||
})[0]
|
||||
link := prepSlackUserLinks(t, db, 1, func(_ int, link *slackapp.UserLink) {
|
||||
link.TeamID = app.TeamID
|
||||
})[0]
|
||||
|
||||
res, err = db.FetchSlackUserLinkByUserID(context.Background(), link.UserID, org)
|
||||
assert.NoError(t, err)
|
||||
testutil.AssertFilterEqual(t, &link, res)
|
||||
}
|
||||
|
||||
func Test_agent_DeleteSlackUserLink(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
Link slackapp.UserLink
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
link := prepSlackUserLinks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
Link: link,
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful delete": func(t *testing.T, db *DB) tcase {
|
||||
link := prepSlackUserLinks(t, db, 1, nil)[0]
|
||||
|
||||
return tcase{
|
||||
Link: link,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.DeleteSlackUserLink(ctx, c.Link.SlackUserID, c.Link.TeamID)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var slackUserID string
|
||||
|
||||
q, args := db.builder.Select("slack_user_id").
|
||||
From("slack_user_links").
|
||||
Where(sq.Eq{
|
||||
"slack_user_id": c.Link.SlackUserID,
|
||||
"fk_team_id": c.Link.TeamID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&slackUserID, q, args...)
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
127
server/core/internal/db/user_test.go
Normal file
127
server/core/internal/db/user_test.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/testutil"
|
||||
"github.com/oxynote/oxynote/server/core/pkg/timeutil"
|
||||
"github.com/rs/xid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func prepUsers(t *testing.T, db *DB, count int) []string {
|
||||
t.Helper()
|
||||
|
||||
res := make([]string, count)
|
||||
|
||||
now := timeutil.Now().Truncate(time.Second)
|
||||
|
||||
for i := range count {
|
||||
id := xid.New().String()
|
||||
|
||||
res[i] = id
|
||||
|
||||
q, args := db.builder.Insert("users").
|
||||
SetMap(map[string]any{
|
||||
"id": id,
|
||||
"name": "User " + strconv.Itoa(i),
|
||||
"email": id + "@test.test",
|
||||
"email_verified": true,
|
||||
"image": "image-" + strconv.Itoa(i),
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}).MustSql()
|
||||
|
||||
_, err := db.sql.Exec(q, args...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func Test_agent_UpdateUserImage(t *testing.T) {
|
||||
type tcase struct {
|
||||
CancelledContext bool
|
||||
UserID string
|
||||
Image string
|
||||
Err error
|
||||
}
|
||||
|
||||
cc := map[string]func(*testing.T, *DB) tcase{
|
||||
"Cancelled context": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
CancelledContext: true,
|
||||
UserID: users[0],
|
||||
Image: "new-image",
|
||||
Err: assert.AnError,
|
||||
}
|
||||
},
|
||||
"Successful update": func(t *testing.T, db *DB) tcase {
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
return tcase{
|
||||
UserID: users[0],
|
||||
Image: "new-image",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for cn, cfn := range cc {
|
||||
t.Run(cn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := prepTempDB(t)
|
||||
c := cfn(t, db)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if c.CancelledContext {
|
||||
cancel()
|
||||
}
|
||||
|
||||
err := db.UpdateUserImage(ctx, c.UserID, c.Image)
|
||||
testutil.RequireEqualError(t, c.Err, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var image string
|
||||
|
||||
q, args := db.builder.Select("image").
|
||||
From("users").
|
||||
Where(sq.Eq{
|
||||
"id": c.UserID,
|
||||
}).MustSql()
|
||||
|
||||
err = db.sql.Get(&image, q, args...)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.Image, image)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_agent_FetchUserName(t *testing.T) {
|
||||
db := prepTempDB(t)
|
||||
|
||||
// error - not found
|
||||
name, err := db.FetchUserName(context.Background(), "non-existent-user-id")
|
||||
testutil.AssertEqualError(t, sql.ErrNoRows, err)
|
||||
assert.Empty(t, name)
|
||||
|
||||
// success
|
||||
users := prepUsers(t, db, 1)
|
||||
|
||||
name, err = db.FetchUserName(context.Background(), users[0])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "User 0", name)
|
||||
}
|
||||
|
|
@ -9,10 +9,14 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -71,6 +75,28 @@ func RequireEqualError(t *testing.T, exp, err error) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// FilterEqual asserts that two objects are equal.
|
||||
// All ignored types found on any of the provided objects will not be compared.
|
||||
func FilterEqual(v1, v2 any, ignoreTypes ...any) error {
|
||||
diff := cmp.Diff(v1, v2, cmpopts.IgnoreTypes(ignoreTypes...),
|
||||
cmp.Exporter(func(reflect.Type) bool { return true }))
|
||||
if diff != "" {
|
||||
return fmt.Errorf("not equal:\n%s", diff)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertFilterEqual asserts that two objects are equal.
|
||||
// All ignored types found on any of the provided objects will not be compared.
|
||||
func AssertFilterEqual(t *testing.T, v1, v2 any, ignoreTypes ...any) {
|
||||
t.Helper()
|
||||
|
||||
if err := FilterEqual(v1, v2, ignoreTypes...); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewBuffer creates a fresh instance of concurrent writer and its buffer.
|
||||
func NewBuffer() (*Writer, *bytes.Buffer) {
|
||||
b := &bytes.Buffer{}
|
||||
|
|
|
|||
53
server/core/pkg/testutil/testutil_test.go
Normal file
53
server/core/pkg/testutil/testutil_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func Test_FilterEqual(t *testing.T) {
|
||||
type object struct {
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// error - values differ
|
||||
err := FilterEqual(object{Name: "a"}, object{Name: "b"})
|
||||
assert.Error(t, err)
|
||||
|
||||
// success - ignored types are not compared
|
||||
err = FilterEqual(
|
||||
object{Name: "a", CreatedAt: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)},
|
||||
object{Name: "a", CreatedAt: time.Date(2022, 3, 4, 0, 0, 0, 0, time.UTC)},
|
||||
time.Time{},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// success - equal instants in different locations are equal
|
||||
err = FilterEqual(
|
||||
object{CreatedAt: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)},
|
||||
object{CreatedAt: time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC).Local()},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_AssertFilterEqual(t *testing.T) {
|
||||
// error - values differ
|
||||
nt := &testing.T{}
|
||||
|
||||
AssertFilterEqual(nt, 1, 2)
|
||||
assert.True(t, nt.Failed())
|
||||
|
||||
// success
|
||||
nt = &testing.T{}
|
||||
|
||||
AssertFilterEqual(nt, 1, 1)
|
||||
assert.False(t, nt.Failed())
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user