多租户更改
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
// Package common contains utility methods used by all adapters.
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/store"
|
||||
t "github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type AuthRecord struct {
|
||||
Unique string `json:"unique" bson:"_id"`
|
||||
UserId string `json:"userid"`
|
||||
Scheme string `json:"scheme"`
|
||||
AuthLvl auth.Level `json:"authLvl"`
|
||||
Secret []byte `json:"secret"`
|
||||
Expires time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
// SelectEarliestUpdatedSubs selects no more than the given number of subscriptions from the
|
||||
// given slice satisfying the query. When the number of subscriptions is greater than the limit,
|
||||
// the subscriptions with the earliest timestamp are selected.
|
||||
func SelectEarliestUpdatedSubs(subs []t.Subscription, opts *t.QueryOpt, maxResults int) []t.Subscription {
|
||||
limit := maxResults
|
||||
ims := time.Time{}
|
||||
if opts != nil {
|
||||
if opts.Limit > 0 && opts.Limit < limit {
|
||||
limit = opts.Limit
|
||||
}
|
||||
if opts.IfModifiedSince != nil {
|
||||
ims = *opts.IfModifiedSince
|
||||
}
|
||||
}
|
||||
|
||||
// No cache management and the number of results is below the limit: return all.
|
||||
if ims.IsZero() && len(subs) <= limit {
|
||||
return subs
|
||||
}
|
||||
|
||||
// Now that we fetched potentially more subscriptions than needed, we got to take those with the oldest modifications.
|
||||
// Sorting in ascending order by modification time.
|
||||
sort.Slice(subs, func(i, j int) bool {
|
||||
return subs[i].LastModified().Before(subs[j].LastModified())
|
||||
})
|
||||
|
||||
if !ims.IsZero() {
|
||||
// Keep only those subscriptions which are newer than ims.
|
||||
at := sort.Search(len(subs), func(i int) bool {
|
||||
return subs[i].LastModified().After(ims)
|
||||
})
|
||||
subs = subs[at:]
|
||||
}
|
||||
// Trim slice at the limit.
|
||||
if len(subs) > limit {
|
||||
subs = subs[:limit]
|
||||
}
|
||||
|
||||
return subs
|
||||
}
|
||||
|
||||
// SelectLatestTime picks the latest update timestamp out of the two.
|
||||
func SelectLatestTime(t1, t2 time.Time) time.Time {
|
||||
if t1.Before(t2) {
|
||||
// Subscription has not changed recently, use user's update timestamp.
|
||||
return t2
|
||||
}
|
||||
|
||||
return t1
|
||||
}
|
||||
|
||||
// RangesToSql converts a slice of ranges to SQL BETWEEN or IN() constraint and arguments.
|
||||
func RangesToSql(in []t.Range) (string, []any) {
|
||||
if len(in) > 1 || in[0].Hi == 0 {
|
||||
var args []any
|
||||
for _, r := range in {
|
||||
if r.Hi == 0 {
|
||||
args = append(args, r.Low)
|
||||
} else {
|
||||
for i := r.Low; i < r.Hi; i++ {
|
||||
args = append(args, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "IN (?" + strings.Repeat(",?", len(args)-1) + ")", args
|
||||
}
|
||||
|
||||
// Optimizing for a special case of single range low..hi.
|
||||
// SQL's BETWEEN is inclusive-inclusive thus decrement Hi by 1.
|
||||
return "BETWEEN ? AND ?", []any{in[0].Low, in[0].Hi - 1}
|
||||
}
|
||||
|
||||
// DisjunctionSql converts a slice of disjunctions to SQL HAVING clause and arguments.
|
||||
func DisjunctionSql(req [][]string, fieldName string) (string, []any) {
|
||||
var args []any
|
||||
counts := make([]string, 0, len(req))
|
||||
for _, reqDisjunction := range req {
|
||||
// At least one of the tags must be present.
|
||||
if len(reqDisjunction) == 0 {
|
||||
continue
|
||||
}
|
||||
counts = append(counts, "COUNT("+fieldName+" IN (?"+strings.Repeat(",?", len(reqDisjunction)-1)+") OR NULL)>=1")
|
||||
for _, tag := range reqDisjunction {
|
||||
args = append(args, tag)
|
||||
}
|
||||
}
|
||||
return "HAVING " + strings.Join(counts, " AND ") + " ", args
|
||||
}
|
||||
|
||||
// FilterFoundTags keeps only those tags in setTags that are present in the index.
|
||||
func FilterFoundTags(setTags t.StringSlice, index map[string]struct{}) []string {
|
||||
foundTags := make([]string, 0, 1)
|
||||
for _, tag := range setTags {
|
||||
if _, ok := index[tag]; ok {
|
||||
foundTags = append(foundTags, tag)
|
||||
}
|
||||
}
|
||||
return foundTags
|
||||
}
|
||||
|
||||
// Convert to JSON before storing to JSON field.
|
||||
func ToJSON(src any) []byte {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
jval, _ := json.Marshal(src)
|
||||
return jval
|
||||
}
|
||||
|
||||
// Deserialize JSON data from DB.
|
||||
func FromJSON(src any) any {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
if bb, ok := src.([]byte); ok {
|
||||
var out any
|
||||
json.Unmarshal(bb, &out)
|
||||
return out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert update to a list of columns and arguments.
|
||||
func UpdateByMap(update map[string]any) (cols []string, args []any) {
|
||||
for col, arg := range update {
|
||||
col = strings.ToLower(col)
|
||||
if col == "public" || col == "trusted" || col == "private" || col == "aux" {
|
||||
arg = ToJSON(arg)
|
||||
}
|
||||
cols = append(cols, col+"=?")
|
||||
args = append(args, arg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If Tags field is updated, get the tags so tags table cab be updated too.
|
||||
func ExtractTags(update map[string]any) []string {
|
||||
var tags []string
|
||||
|
||||
if val := update["Tags"]; val != nil {
|
||||
tags, _ = val.(t.StringSlice)
|
||||
}
|
||||
|
||||
return []string(tags)
|
||||
}
|
||||
|
||||
// EncodeUidString takes decoded string representation of int64, produce UID.
|
||||
// UIDs are stored as decoded int64 values.
|
||||
func EncodeUidString(str string) t.Uid {
|
||||
unum, _ := strconv.ParseInt(str, 10, 64)
|
||||
return store.EncodeUid(unum)
|
||||
}
|
||||
|
||||
// DecodeUidString takes UID as string, converts it to int64 representation.
|
||||
// UIDs are stored as decoded int64 values.
|
||||
func DecodeUidString(str string) int64 {
|
||||
uid := t.ParseUid(str)
|
||||
return store.DecodeUid(uid)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func genTestData() []types.Subscription {
|
||||
var testData = []types.Subscription{
|
||||
{ObjHeader: types.ObjHeader{Id: "1", UpdatedAt: time.Date(2021, time.June, 1, 1, 11, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "2", UpdatedAt: time.Date(2021, time.June, 2, 2, 12, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "3", UpdatedAt: time.Date(2021, time.June, 3, 3, 13, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "4", UpdatedAt: time.Date(2021, time.June, 4, 4, 14, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "5", UpdatedAt: time.Date(2021, time.June, 5, 5, 15, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "6", UpdatedAt: time.Date(2021, time.June, 6, 6, 16, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "7", UpdatedAt: time.Date(2021, time.June, 7, 7, 17, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "8", UpdatedAt: time.Date(2021, time.June, 8, 8, 18, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "9", UpdatedAt: time.Date(2021, time.June, 9, 9, 19, 0, 0, time.Local)}},
|
||||
{ObjHeader: types.ObjHeader{Id: "10", UpdatedAt: time.Date(2021, time.June, 10, 10, 20, 0, 0, time.Local)}},
|
||||
}
|
||||
|
||||
// TouchedAt is either greater or equal to UpdatedAt.
|
||||
testData[0].SetTouchedAt(time.Date(2021, time.June, 1, 1, 11, 0, 0, time.Local)) // 1
|
||||
testData[1].SetTouchedAt(time.Date(2021, time.June, 4, 4, 12, 0, 0, time.Local)) // 3
|
||||
testData[2].SetTouchedAt(time.Date(2021, time.June, 4, 2, 13, 0, 0, time.Local)) // 2
|
||||
testData[3].SetTouchedAt(time.Date(2021, time.June, 4, 4, 14, 0, 0, time.Local)) // 4
|
||||
testData[4].SetTouchedAt(time.Date(2021, time.June, 7, 5, 15, 0, 0, time.Local)) // 6
|
||||
testData[5].SetTouchedAt(time.Date(2021, time.June, 6, 6, 16, 0, 0, time.Local)) // 5
|
||||
testData[6].SetTouchedAt(time.Date(2021, time.June, 7, 7, 17, 0, 0, time.Local)) // 7
|
||||
testData[7].SetTouchedAt(time.Date(2021, time.June, 9, 8, 18, 0, 0, time.Local)) // 8
|
||||
testData[8].SetTouchedAt(time.Date(2021, time.June, 10, 11, 19, 0, 0, time.Local)) // 10
|
||||
testData[9].SetTouchedAt(time.Date(2021, time.June, 10, 10, 20, 0, 0, time.Local)) // 9
|
||||
|
||||
return testData
|
||||
}
|
||||
|
||||
func TestSelectEarliestUpdatedSubs(t *testing.T) {
|
||||
getOrder := func(subs []types.Subscription) string {
|
||||
var order []string
|
||||
for i := range subs {
|
||||
order = append(order, subs[i].Id)
|
||||
}
|
||||
return strings.Join(order, ",")
|
||||
}
|
||||
|
||||
subs := SelectEarliestUpdatedSubs(genTestData(), nil, 100)
|
||||
|
||||
// No sorting when returning the full set.
|
||||
expectedOrder := "1,2,3,4,5,6,7,8,9,10"
|
||||
sortOrder := getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Wrong results returned. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
// Sorted, oldest 9 results.
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), nil, 9)
|
||||
expectedOrder = "1,3,2,4,6,5,7,8,10"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Limited query returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
// Sorted, oldest 9 results.
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 20}, 9)
|
||||
expectedOrder = "1,3,2,4,6,5,7,8,10"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Limited query (2) returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
// Sorted, oldest 9 results.
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 9}, 20)
|
||||
expectedOrder = "1,3,2,4,6,5,7,8,10"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Limited query (3) returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
ims := time.Date(2021, time.June, 7, 8, 16, 15, 0, time.Local)
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 6, IfModifiedSince: &ims}, 20)
|
||||
expectedOrder = "8,10,9"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Date & count limited query returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
|
||||
ims = time.Date(2021, time.June, 4, 4, 13, 15, 0, time.Local)
|
||||
subs = SelectEarliestUpdatedSubs(genTestData(), &types.QueryOpt{Limit: 3, IfModifiedSince: &ims}, 20)
|
||||
expectedOrder = "4,6,5"
|
||||
sortOrder = getOrder(subs)
|
||||
if sortOrder != expectedOrder {
|
||||
t.Error("Count & date limited query returned wrong results. Expected:", expectedOrder, "; Got:", sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectLatestTime(t *testing.T) {
|
||||
t1 := time.Date(2021, time.June, 1, 10, 0, 0, 0, time.UTC)
|
||||
t2 := time.Date(2021, time.June, 2, 10, 0, 0, 0, time.UTC)
|
||||
|
||||
// t1 is before t2, should return t2
|
||||
result := SelectLatestTime(t1, t2)
|
||||
if !result.Equal(t2) {
|
||||
t.Errorf("Expected %v, got %v", t2, result)
|
||||
}
|
||||
|
||||
// t2 is after t1, should return t2
|
||||
result = SelectLatestTime(t2, t1)
|
||||
if !result.Equal(t2) {
|
||||
t.Errorf("Expected %v, got %v", t2, result)
|
||||
}
|
||||
|
||||
// Equal times, should return either one (in this case t1)
|
||||
result = SelectLatestTime(t1, t1)
|
||||
if !result.Equal(t1) {
|
||||
t.Errorf("Expected %v, got %v", t1, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangesToSql(t *testing.T) {
|
||||
// Test single range with Hi = 0 (IN clause)
|
||||
ranges := []types.Range{{Low: 5, Hi: 0}}
|
||||
sql, args := RangesToSql(ranges)
|
||||
expectedSql := "IN (?)"
|
||||
expectedArgs := []any{5}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test single range with Hi > 0 (BETWEEN clause)
|
||||
ranges = []types.Range{{Low: 5, Hi: 8}}
|
||||
sql, args = RangesToSql(ranges)
|
||||
expectedSql = "BETWEEN ? AND ?"
|
||||
expectedArgs = []any{5, 7} // Hi-1 for BETWEEN
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test multiple ranges (IN clause)
|
||||
ranges = []types.Range{{Low: 1, Hi: 3}, {Low: 5, Hi: 0}, {Low: 8, Hi: 10}}
|
||||
sql, args = RangesToSql(ranges)
|
||||
expectedSql = "IN (?,?,?,?,?)"
|
||||
expectedArgs = []any{1, 2, 5, 8, 9}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisjunctionSql(t *testing.T) {
|
||||
// Test single disjunction
|
||||
req := [][]string{{"tag1", "tag2", "tag3"}}
|
||||
sql, args := DisjunctionSql(req, "tagname")
|
||||
expectedSql := "HAVING COUNT(tagname IN (?,?,?) OR NULL)>=1 "
|
||||
expectedArgs := []any{"tag1", "tag2", "tag3"}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test multiple disjunctions
|
||||
req = [][]string{{"tag1", "tag2"}, {"tag3"}, {"tag4", "tag5"}}
|
||||
sql, args = DisjunctionSql(req, "fieldname")
|
||||
expectedSql = "HAVING COUNT(fieldname IN (?,?) OR NULL)>=1 AND COUNT(fieldname IN (?) OR NULL)>=1 AND COUNT(fieldname IN (?,?) OR NULL)>=1 "
|
||||
expectedArgs = []any{"tag1", "tag2", "tag3", "tag4", "tag5"}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
|
||||
// Test empty disjunctions (should be skipped)
|
||||
req = [][]string{{"tag1"}, {}, {"tag2"}}
|
||||
sql, args = DisjunctionSql(req, "fieldname")
|
||||
expectedSql = "HAVING COUNT(fieldname IN (?) OR NULL)>=1 AND COUNT(fieldname IN (?) OR NULL)>=1 "
|
||||
expectedArgs = []any{"tag1", "tag2"}
|
||||
if sql != expectedSql {
|
||||
t.Errorf("Expected SQL '%s', got '%s'", expectedSql, sql)
|
||||
}
|
||||
if !reflect.DeepEqual(args, expectedArgs) {
|
||||
t.Errorf("Expected args %v, got %v", expectedArgs, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterFoundTags(t *testing.T) {
|
||||
setTags := types.StringSlice{"tag1", "tag2", "tag3", "tag4", "tag5"}
|
||||
index := map[string]struct{}{
|
||||
"tag1": {},
|
||||
"tag3": {},
|
||||
"tag5": {},
|
||||
"tag6": {}, // Not in setTags
|
||||
}
|
||||
|
||||
result := FilterFoundTags(setTags, index)
|
||||
expected := []string{"tag1", "tag3", "tag5"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with empty index
|
||||
emptyIndex := map[string]struct{}{}
|
||||
result = FilterFoundTags(setTags, emptyIndex)
|
||||
expected = []string{}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with empty setTags
|
||||
emptyTags := types.StringSlice{}
|
||||
result = FilterFoundTags(emptyTags, index)
|
||||
expected = []string{}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
// Test with nil
|
||||
result := ToJSON(nil)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil, got %v", result)
|
||||
}
|
||||
|
||||
// Test with string
|
||||
input := "test string"
|
||||
result = ToJSON(input)
|
||||
expected := []byte(`"test string"`)
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with map
|
||||
input2 := map[string]any{"key": "value", "number": 42}
|
||||
result = ToJSON(input2)
|
||||
// Parse back to verify
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(result, &parsed); err != nil {
|
||||
t.Errorf("Failed to unmarshal result: %v", err)
|
||||
}
|
||||
if parsed["key"] != "value" || parsed["number"] != float64(42) {
|
||||
t.Errorf("JSON conversion failed, got %v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromJSON(t *testing.T) {
|
||||
// Test with nil
|
||||
result := FromJSON(nil)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil, got %v", result)
|
||||
}
|
||||
|
||||
// Test with valid JSON bytes
|
||||
input := []byte(`{"key": "value", "number": 42}`)
|
||||
result = FromJSON(input)
|
||||
if resultMap, ok := result.(map[string]any); ok {
|
||||
if resultMap["key"] != "value" || resultMap["number"] != float64(42) {
|
||||
t.Errorf("JSON deserialization failed, got %v", resultMap)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Expected map[string]any, got %T", result)
|
||||
}
|
||||
|
||||
// Test with invalid JSON bytes
|
||||
invalidInput := []byte(`{invalid json}`)
|
||||
result = FromJSON(invalidInput)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for invalid JSON, got %v", result)
|
||||
}
|
||||
|
||||
// Test with non-byte slice
|
||||
stringInput := "not bytes"
|
||||
result = FromJSON(stringInput)
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for non-byte input, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateByMap(t *testing.T) {
|
||||
update := map[string]any{
|
||||
"Name": "John Doe",
|
||||
"Age": 30,
|
||||
"Public": map[string]string{"avatar": "url"},
|
||||
"Private": map[string]string{"email": "john@example.com"},
|
||||
"Trusted": map[string]bool{"verified": true},
|
||||
"UpdatedAt": time.Now(),
|
||||
}
|
||||
|
||||
cols, args := UpdateByMap(update)
|
||||
|
||||
// Check that we have the right number of columns and args
|
||||
if len(cols) != len(args) || len(cols) != len(update) {
|
||||
t.Errorf("Expected %d columns and args, got %d cols and %d args", len(update), len(cols), len(args))
|
||||
}
|
||||
|
||||
// Verify column format
|
||||
for _, col := range cols {
|
||||
if !strings.Contains(col, "=?") {
|
||||
t.Errorf("Column should contain '=?', got %s", col)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that JSON fields are properly handled
|
||||
foundPublic := false
|
||||
foundPrivate := false
|
||||
foundTrusted := false
|
||||
for i, col := range cols {
|
||||
if strings.HasPrefix(col, "public=?") {
|
||||
foundPublic = true
|
||||
// Should be JSON bytes
|
||||
if _, ok := args[i].([]byte); !ok {
|
||||
t.Errorf("Public field should be []byte, got %T", args[i])
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(col, "private=?") {
|
||||
foundPrivate = true
|
||||
if _, ok := args[i].([]byte); !ok {
|
||||
t.Errorf("Private field should be []byte, got %T", args[i])
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(col, "trusted=?") {
|
||||
foundTrusted = true
|
||||
if _, ok := args[i].([]byte); !ok {
|
||||
t.Errorf("Trusted field should be []byte, got %T", args[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundPublic || !foundPrivate || !foundTrusted {
|
||||
t.Error("Missing JSON fields in output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTags(t *testing.T) {
|
||||
// Test with Tags field present
|
||||
update := map[string]any{
|
||||
"Name": "John",
|
||||
"Tags": types.StringSlice{"tag1", "tag2", "tag3"},
|
||||
"Age": 30,
|
||||
}
|
||||
tags := ExtractTags(update)
|
||||
expected := []string{"tag1", "tag2", "tag3"}
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, tags)
|
||||
}
|
||||
|
||||
// Test with no Tags field
|
||||
update = map[string]any{
|
||||
"Name": "John",
|
||||
"Age": 30,
|
||||
}
|
||||
tags = ExtractTags(update)
|
||||
expected = nil
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, tags)
|
||||
}
|
||||
|
||||
// Test with nil Tags field
|
||||
update = map[string]any{
|
||||
"Name": "John",
|
||||
"Tags": nil,
|
||||
"Age": 30,
|
||||
}
|
||||
tags = ExtractTags(update)
|
||||
expected = nil
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, tags)
|
||||
}
|
||||
|
||||
// Test with wrong type for Tags field
|
||||
update = map[string]any{
|
||||
"Name": "John",
|
||||
"Tags": "not a slice",
|
||||
"Age": 30,
|
||||
}
|
||||
tags = ExtractTags(update)
|
||||
expected = nil
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Expected %+v, got %+v", expected, tags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package test_data
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tinode/chat/server/auth"
|
||||
"github.com/tinode/chat/server/db/common"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type TestData struct {
|
||||
UGen *types.UidGenerator
|
||||
Users []*types.User
|
||||
Creds []*types.Credential
|
||||
Recs []common.AuthRecord
|
||||
Topics []*types.Topic
|
||||
Subs []*types.Subscription
|
||||
Msgs []*types.Message
|
||||
Devs []*types.DeviceDef
|
||||
Files []*types.FileDef
|
||||
// Tags: add, remove, reset
|
||||
Tags [][]string
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
func initUsers(now time.Time) []*types.User {
|
||||
users := make([]*types.User, 0, 3)
|
||||
users = append(users, &types.User{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "3ysxkod5hNM",
|
||||
},
|
||||
UserAgent: "SomeAgent v1.2.3",
|
||||
Tags: []string{"alice"},
|
||||
})
|
||||
users = append(users, &types.User{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "9AVDamaNCRY",
|
||||
},
|
||||
UserAgent: "Tinode Web v111.222.333",
|
||||
Tags: []string{"bob"},
|
||||
})
|
||||
users = append(users, &types.User{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "0QLrX3WPS2o",
|
||||
},
|
||||
UserAgent: "Tindroid v1.2.3",
|
||||
Tags: []string{"carol"},
|
||||
})
|
||||
for _, user := range users {
|
||||
// Initialize timestamps.
|
||||
user.InitTimes()
|
||||
// Assign user.id from user.Id.
|
||||
user.Uid()
|
||||
}
|
||||
deletedAt := now.Add(10 * time.Minute)
|
||||
users[2].State = types.StateDeleted
|
||||
users[2].StateAt = &deletedAt
|
||||
return users
|
||||
}
|
||||
|
||||
func initCreds(now time.Time, users []*types.User) []*types.Credential {
|
||||
creds := make([]*types.Credential, 0, 6)
|
||||
creds = append(creds, &types.Credential{ // 0
|
||||
User: users[0].Id,
|
||||
Method: "email",
|
||||
Value: "alice@test.example.com",
|
||||
Done: true,
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 1
|
||||
User: users[1].Id,
|
||||
Method: "email",
|
||||
Value: "bob@test.example.com",
|
||||
Done: true,
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 2
|
||||
User: users[1].Id,
|
||||
Method: "email",
|
||||
Value: "bob@test.example.com",
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 3
|
||||
User: users[2].Id,
|
||||
Method: "tel",
|
||||
Value: "+998991112233",
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 4
|
||||
User: users[2].Id,
|
||||
Method: "tel",
|
||||
Value: "+998993332211",
|
||||
Done: true,
|
||||
})
|
||||
creds = append(creds, &types.Credential{ // 5
|
||||
User: users[2].Id,
|
||||
Method: "email",
|
||||
Value: "asdf@example.com",
|
||||
})
|
||||
for _, cred := range creds {
|
||||
cred.InitTimes()
|
||||
}
|
||||
creds[3].CreatedAt = now.Add(-10 * time.Minute)
|
||||
creds[3].UpdatedAt = now.Add(-10 * time.Minute)
|
||||
return creds
|
||||
}
|
||||
|
||||
func initAuthRecords(now time.Time, users []*types.User) []common.AuthRecord {
|
||||
recs := make([]common.AuthRecord, 0, 2)
|
||||
recs = append(recs, common.AuthRecord{
|
||||
Unique: "basic:alice",
|
||||
UserId: users[0].Id,
|
||||
Scheme: "basic",
|
||||
AuthLvl: auth.LevelAuth,
|
||||
Secret: []byte{'a', 'l', 'i', 'c', 'e'},
|
||||
Expires: now.Add(24 * time.Hour),
|
||||
})
|
||||
recs = append(recs, common.AuthRecord{
|
||||
Unique: "basic:bob",
|
||||
UserId: users[1].Id,
|
||||
Scheme: "basic",
|
||||
AuthLvl: auth.LevelAuth,
|
||||
Secret: []byte{'b', 'o', 'b'},
|
||||
Expires: now.Add(24 * time.Hour),
|
||||
})
|
||||
return recs
|
||||
}
|
||||
|
||||
func initTopics(now time.Time, users []*types.User) []*types.Topic {
|
||||
topics := make([]*types.Topic, 0, 5)
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "grpgRXf0rU4uR4",
|
||||
CreatedAt: now.Add(10 * time.Minute),
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
Owner: users[0].Id,
|
||||
SeqId: 111,
|
||||
Tags: []string{"travel", "zxcv"},
|
||||
SubCnt: 2,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2p9AVDamaNCRbfKzGSh3mE0w",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 12,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2pxQLrX3WPS2rfKzGSh3mE0w",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 15,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2pE1iE7I9JN5ESv44HiLbj1A",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 555,
|
||||
})
|
||||
topics = append(topics, &types.Topic{
|
||||
ObjHeader: types.ObjHeader{
|
||||
Id: "p2pQvr1xwKU01LfKzGSh3mE0w",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
TouchedAt: now,
|
||||
SeqId: 333,
|
||||
})
|
||||
return topics
|
||||
}
|
||||
|
||||
func initSubs(now time.Time, users []*types.User, topics []*types.Topic) []*types.Subscription {
|
||||
subs := make([]*types.Subscription, 0, 6)
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(10 * time.Minute),
|
||||
},
|
||||
User: users[0].Id,
|
||||
Topic: topics[0].Id,
|
||||
RecvSeqId: 5,
|
||||
ReadSeqId: 1,
|
||||
ModeWant: 255,
|
||||
ModeGiven: 255,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(15 * time.Minute),
|
||||
},
|
||||
User: users[1].Id,
|
||||
Topic: topics[0].Id,
|
||||
RecvSeqId: 6,
|
||||
ReadSeqId: 3,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now.Add(-10 * time.Hour),
|
||||
UpdatedAt: now.Add(-10 * time.Hour),
|
||||
},
|
||||
User: users[0].Id,
|
||||
Topic: topics[1].Id,
|
||||
RecvSeqId: 9,
|
||||
ReadSeqId: 5,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(20 * time.Minute),
|
||||
},
|
||||
User: users[1].Id,
|
||||
Topic: topics[1].Id,
|
||||
RecvSeqId: 9,
|
||||
ReadSeqId: 5,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(30 * time.Minute),
|
||||
},
|
||||
User: users[2].Id,
|
||||
Topic: topics[2].Id,
|
||||
RecvSeqId: 0,
|
||||
ReadSeqId: 0,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
subs = append(subs, &types.Subscription{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now.Add(40 * time.Minute),
|
||||
},
|
||||
User: users[2].Id,
|
||||
Topic: topics[3].Id,
|
||||
RecvSeqId: 555,
|
||||
ReadSeqId: 455,
|
||||
ModeWant: 47,
|
||||
ModeGiven: 47,
|
||||
})
|
||||
for _, sub := range subs {
|
||||
sub.SetTouchedAt(now)
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
func initMessages(users []*types.User, topics []*types.Topic) []*types.Message {
|
||||
msgs := make([]*types.Message, 0, 6)
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 1,
|
||||
Topic: topics[0].Id,
|
||||
From: users[0].Id,
|
||||
Content: "msg1",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 2,
|
||||
Topic: topics[0].Id,
|
||||
From: users[2].Id,
|
||||
Content: "msg2",
|
||||
DeletedFor: []types.SoftDelete{{
|
||||
User: users[0].Id,
|
||||
DelId: 1}},
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 3,
|
||||
Topic: topics[0].Id,
|
||||
From: users[0].Id,
|
||||
Content: "msg31",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 1,
|
||||
Topic: topics[1].Id,
|
||||
From: users[1].Id,
|
||||
Content: "msg1",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 5,
|
||||
Topic: topics[1].Id,
|
||||
From: users[1].Id,
|
||||
Content: "msg2",
|
||||
})
|
||||
msgs = append(msgs, &types.Message{
|
||||
SeqId: 11,
|
||||
Topic: topics[1].Id,
|
||||
From: users[0].Id,
|
||||
Content: "msg3",
|
||||
})
|
||||
|
||||
for i, msg := range msgs {
|
||||
msg.InitTimes()
|
||||
msg.SetUid(types.Uid(i + 1))
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func initDevices(now time.Time) []*types.DeviceDef {
|
||||
devs := make([]*types.DeviceDef, 0, 2)
|
||||
devs = append(devs, &types.DeviceDef{
|
||||
DeviceId: "2934ujfoviwj09ntf094",
|
||||
Platform: "Android",
|
||||
LastSeen: now,
|
||||
Lang: "en_EN",
|
||||
})
|
||||
devs = append(devs, &types.DeviceDef{
|
||||
DeviceId: "pogpjb023b09gfdmp",
|
||||
Platform: "iOS",
|
||||
LastSeen: now,
|
||||
Lang: "en_EN",
|
||||
})
|
||||
return devs
|
||||
}
|
||||
|
||||
func initFileDefs(now time.Time, users []*types.User) []*types.FileDef {
|
||||
files := make([]*types.FileDef, 0, 2)
|
||||
files = append(files, &types.FileDef{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Status: types.UploadStarted,
|
||||
User: users[0].Id,
|
||||
MimeType: "application/pdf",
|
||||
Location: "uploads/qwerty.pdf",
|
||||
Size: 123456,
|
||||
})
|
||||
files = append(files, &types.FileDef{
|
||||
ObjHeader: types.ObjHeader{
|
||||
CreatedAt: now.Add(60 * time.Minute),
|
||||
UpdatedAt: now.Add(60 * time.Minute),
|
||||
},
|
||||
Status: types.UploadStarted,
|
||||
User: users[0].Id,
|
||||
Location: "uploads/asdf.txt",
|
||||
Size: 654321,
|
||||
})
|
||||
files[0].SetUid(types.Uid(1001))
|
||||
files[1].SetUid(types.Uid(1002))
|
||||
return files
|
||||
}
|
||||
|
||||
func initTags() [][]string {
|
||||
// Tags must be lowercase and non-repeating.
|
||||
addTags := []string{"tag1", "alice"}
|
||||
removeTags := []string{"alice", "tag1", "tag2"}
|
||||
resetTags := []string{"alice", "tag111", "tag333"}
|
||||
return [][]string{addTags, removeTags, resetTags}
|
||||
}
|
||||
|
||||
func InitTestData() *TestData {
|
||||
// Use fixed timestamp to make tests more predictable
|
||||
var now = time.Date(2021, time.June, 12, 11, 39, 24, 15, time.Local).UTC().Round(time.Millisecond)
|
||||
var uGen = &types.UidGenerator{}
|
||||
if err := uGen.Init(11, []byte("testtesttesttest")); err != nil {
|
||||
return nil
|
||||
}
|
||||
var users = initUsers(now)
|
||||
var topics = initTopics(now, users)
|
||||
return &TestData{
|
||||
UGen: uGen,
|
||||
Users: users,
|
||||
Creds: initCreds(now, users),
|
||||
Recs: initAuthRecords(now, users),
|
||||
Topics: topics,
|
||||
Subs: initSubs(now, users, topics),
|
||||
Msgs: initMessages(users, topics),
|
||||
Devs: initDevices(now),
|
||||
Files: initFileDefs(now, users),
|
||||
Tags: initTags(),
|
||||
Now: now,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user