1838 lines
54 KiB
Go
1838 lines
54 KiB
Go
// To test another db backend:
|
|
// 1) Create GetAdapter function inside your db backend adapter package (like one inside mysql adapter)
|
|
// 2) Uncomment your db backend package ('backend' named package)
|
|
// 3) Write own initConnectionToDb and 'db' variable
|
|
// 4) Replace mysql specific db queries inside test to your own queries.
|
|
// 5) Run.
|
|
|
|
package tests
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/tinode/chat/server/auth"
|
|
adapter "github.com/tinode/chat/server/db"
|
|
"github.com/tinode/chat/server/store"
|
|
jcr "github.com/tinode/jsonco"
|
|
|
|
"github.com/tinode/chat/server/db/common/test_data"
|
|
backend "github.com/tinode/chat/server/db/mysql"
|
|
"github.com/tinode/chat/server/logs"
|
|
"github.com/tinode/chat/server/store/types"
|
|
)
|
|
|
|
type configType struct {
|
|
// If Reset=true test will recreate database every time it runs
|
|
Reset bool `json:"reset_db_data"`
|
|
// Configurations for individual adapters.
|
|
Adapters map[string]json.RawMessage `json:"adapters"`
|
|
}
|
|
|
|
var config configType
|
|
var adp adapter.Adapter
|
|
var db *sqlx.DB
|
|
var testData *test_data.TestData
|
|
|
|
var dummyUid1 = types.Uid(12345)
|
|
var dummyUid2 = types.Uid(54321)
|
|
|
|
func TestCreateDb(t *testing.T) {
|
|
if err := adp.CreateDb(config.Reset); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Saved db is closed, get a fresh one.
|
|
db = adp.GetTestDB().(*sqlx.DB)
|
|
}
|
|
|
|
func TestGroupBusinessTablesCreated(t *testing.T) {
|
|
tables := []string{
|
|
"im_group_settings", "im_group_members", "im_group_invitations", "im_group_join_requests",
|
|
"im_group_announcements", "im_group_announcement_reads", "im_group_mutes", "im_group_qrcodes",
|
|
"im_group_files", "im_group_admin_logs", "im_group_idempotency",
|
|
}
|
|
args := make([]any, len(tables))
|
|
for i, table := range tables {
|
|
args[i] = table
|
|
}
|
|
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(tables)), ",")
|
|
|
|
var count int
|
|
query := "SELECT COUNT(*) FROM information_schema.tables " +
|
|
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ")"
|
|
if err := db.Get(&count, query, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != len(tables) {
|
|
t.Fatalf("group business table count: got %d, want %d", count, len(tables))
|
|
}
|
|
|
|
query = "SELECT COUNT(*) FROM information_schema.columns " +
|
|
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ") AND column_name='tenant_id'"
|
|
if err := db.Get(&count, query, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != len(tables) {
|
|
t.Fatalf("tenant business tables with tenant_id: got %d, want %d", count, len(tables))
|
|
}
|
|
}
|
|
|
|
func TestHTTPInfrastructureTablesCreated(t *testing.T) {
|
|
tables := []string{"im_http_idempotency"}
|
|
args := make([]any, len(tables))
|
|
for i, table := range tables {
|
|
args[i] = table
|
|
}
|
|
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(tables)), ",")
|
|
|
|
var count int
|
|
query := "SELECT COUNT(*) FROM information_schema.tables " +
|
|
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ")"
|
|
if err := db.Get(&count, query, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != len(tables) {
|
|
t.Fatalf("HTTP infrastructure table count: got %d, want %d", count, len(tables))
|
|
}
|
|
|
|
query = "SELECT COUNT(*) FROM information_schema.columns " +
|
|
"WHERE table_schema=DATABASE() AND table_name IN (" + placeholders + ") AND column_name='tenant_id'"
|
|
if err := db.Get(&count, query, args...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != len(tables) {
|
|
t.Fatalf("HTTP infrastructure tenant_id columns: got %d, want %d", count, len(tables))
|
|
}
|
|
}
|
|
|
|
func TestUserIdentitySchema(t *testing.T) {
|
|
var count int
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_users' AND column_name='phone'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatal("im_users.phone is missing")
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_users' AND column_name='username' AND is_nullable='NO'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatal("im_users.username is missing or nullable")
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_auth' AND column_name IN ('uname','scheme')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatal("legacy im_auth.uname or im_auth.scheme still exists")
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_user_profiles' AND column_name='email'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatal("im_user_profiles.email is missing")
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_users' AND column_name='tags'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatal("legacy im_users.tags still exists")
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.tables
|
|
WHERE table_schema=DATABASE() AND table_name='im_usertags'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatal("legacy im_usertags still exists")
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.tables
|
|
WHERE table_schema=DATABASE() AND table_name='im_topictags'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatal("topic tag table must be retained")
|
|
}
|
|
}
|
|
|
|
func TestAllIMTablesUseIDPrimaryKey(t *testing.T) {
|
|
var tables []string
|
|
if err := db.Select(&tables, `SELECT t.table_name
|
|
FROM information_schema.tables AS t
|
|
WHERE t.table_schema=DATABASE() AND t.table_type='BASE TABLE' AND t.table_name LIKE 'im\_%'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns AS c
|
|
WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='id'
|
|
)
|
|
ORDER BY t.table_name`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tables) != 0 {
|
|
t.Fatalf("IM tables without id column: %v", tables)
|
|
}
|
|
|
|
tables = nil
|
|
if err := db.Select(&tables, `SELECT s.table_name
|
|
FROM information_schema.statistics AS s
|
|
WHERE s.table_schema=DATABASE() AND s.index_name='PRIMARY'
|
|
GROUP BY s.table_name
|
|
HAVING GROUP_CONCAT(s.column_name ORDER BY s.seq_in_index) <> 'id'
|
|
ORDER BY s.table_name`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tables) != 0 {
|
|
t.Fatalf("IM tables whose primary key is not id: %v", tables)
|
|
}
|
|
|
|
targets := make([]string, 0, 11)
|
|
if err := db.Select(&targets, `SELECT table_name
|
|
FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND column_name='id' AND extra LIKE '%auto_increment%'
|
|
AND table_name IN ('im_user_security','im_kvmeta','im_user_profiles','im_user_privacy_settings',
|
|
'im_device_push_tokens','im_friendships','im_user_blocks','im_youth_mode_settings',
|
|
'im_group_settings','im_group_announcement_reads','im_group_mutes')
|
|
ORDER BY table_name`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(targets) != 0 {
|
|
t.Fatalf("snowflake primary keys unexpectedly use AUTO_INCREMENT: %v", targets)
|
|
}
|
|
}
|
|
|
|
func TestTenantAuthIsolation(t *testing.T) {
|
|
tenantAdp, ok := adp.(adapter.TenantAuthAdapter)
|
|
if !ok {
|
|
t.Fatal("MySQL adapter does not implement TenantAuthAdapter")
|
|
}
|
|
var routingConfig struct {
|
|
Enabled bool `json:"tenant_db_routing"`
|
|
}
|
|
if err := json.Unmarshal(config.Adapters[adp.GetName()], &routingConfig); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !routingConfig.Enabled {
|
|
t.Skip("tenant_db_routing is disabled in the MySQL test configuration")
|
|
}
|
|
router, ok := adp.(adapter.TenantRoutedAdapter)
|
|
if !ok {
|
|
t.Fatal("MySQL adapter does not implement TenantRoutedAdapter")
|
|
}
|
|
|
|
now := types.TimeNow()
|
|
suffix := fmt.Sprintf("%d", now.UnixNano())
|
|
dbNames := []string{"im_tenant_auth_a_" + suffix, "im_tenant_auth_b_" + suffix}
|
|
quoteIdent := func(value string) string {
|
|
return "`" + strings.ReplaceAll(value, "`", "``") + "`"
|
|
}
|
|
var baseDB string
|
|
if err := db.Get(&baseDB, "SELECT DATABASE()"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM im_tenant WHERE code IN (?,?)", "test-im_auth-a-"+suffix, "test-im_auth-b-"+suffix)
|
|
for _, dbName := range dbNames {
|
|
db.Exec("DROP DATABASE IF EXISTS " + quoteIdent(dbName))
|
|
}
|
|
})
|
|
|
|
for _, dbName := range dbNames {
|
|
if _, err := db.Exec("CREATE DATABASE " + quoteIdent(dbName) +
|
|
" CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, table := range []string{"im_users", "im_auth", "im_credentials"} {
|
|
query := "CREATE TABLE " + quoteIdent(dbName) + "." + quoteIdent(table) +
|
|
" LIKE " + quoteIdent(baseDB) + "." + quoteIdent(table)
|
|
if _, err := db.Exec(query); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
createTenant := func(code, _ string) types.TenantID {
|
|
var id int64
|
|
if err := db.Get(&id, "SELECT COALESCE(MAX(id),0)+1 FROM im_tenant"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := db.Exec("INSERT INTO im_tenant(id,code,name,state,created_by,updated_by) VALUES(?,?,?,?,?,?)",
|
|
id, code+suffix, code, types.TenantStateActive, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return types.TenantID(id)
|
|
}
|
|
tenantA := createTenant("test-im_auth-a-", dbNames[0])
|
|
tenantB := createTenant("test-im_auth-b-", dbNames[1])
|
|
businessA, _, err := router.TenantAdapterFor(tenantA)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reflect.ValueOf(businessA).Pointer() == reflect.ValueOf(adp).Pointer() {
|
|
t.Skip("tenant_db_routing is disabled in the MySQL test configuration")
|
|
}
|
|
|
|
userAID := now.UnixNano() & 0x3fffffffffffffff
|
|
userBID := userAID + 1
|
|
for index, userID := range []int64{userAID, userBID} {
|
|
tenantDB := quoteIdent(dbNames[index])
|
|
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,account_no,username,createdat,updatedat,state,access,phone,trusted) "+
|
|
"VALUES(?,?,?,?,?,?,?,?,?)", userID, 1000+index, "alice", now, now, types.StateOK, `{}`, "13800000000", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(userid,authlvl,secret) VALUES(?,?,?)",
|
|
userID, auth.LevelAuth, []byte("hash")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_credentials(createdat,updatedat,method,value,synthetic,userid,resp,done) "+
|
|
"VALUES(?,?,?,?,?,?,?,?)", now, now, "tel", "13800000000", "tel:13800000000", userID, "", true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
uidA, _, _, _, err := tenantAdp.TenantAuthGetByLogin(tenantA, "alice")
|
|
if err != nil || uidA != store.EncodeUid(userAID) {
|
|
t.Fatalf("tenant A im_auth lookup: uid=%v err=%v", uidA, err)
|
|
}
|
|
uidB, _, _, _, err := tenantAdp.TenantAuthGetByLogin(tenantB, "alice")
|
|
if err != nil || uidB != store.EncodeUid(userBID) {
|
|
t.Fatalf("tenant B im_auth lookup: uid=%v err=%v", uidB, err)
|
|
}
|
|
credA, err := tenantAdp.TenantUserGetByCred(tenantA, "tel", "13800000000")
|
|
if err != nil || credA != uidA {
|
|
t.Fatalf("tenant A credential lookup: uid=%v err=%v", credA, err)
|
|
}
|
|
credB, err := tenantAdp.TenantUserGetByCred(tenantB, "tel", "13800000000")
|
|
if err != nil || credB != uidB {
|
|
t.Fatalf("tenant B credential lookup: uid=%v err=%v", credB, err)
|
|
}
|
|
|
|
if err = tenantAdp.TenantAuthUpdateRecord(tenantA, uidB, "changed",
|
|
auth.LevelAuth, nil, time.Time{}); err != types.ErrNotFound {
|
|
t.Fatalf("cross-tenant im_auth update: got %v, want %v", err, types.ErrNotFound)
|
|
}
|
|
}
|
|
|
|
func TestTenantUserSearchMatchesUsernamePhoneAndPublicAccount(t *testing.T) {
|
|
businessAdp, ok := adp.(adapter.TenantBusinessAdapter)
|
|
if !ok {
|
|
t.Fatal("MySQL adapter does not implement TenantBusinessAdapter")
|
|
}
|
|
var tenantID types.TenantID
|
|
if err := db.Get(&tenantID, "SELECT id FROM im_tenant WHERE code='default'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
now := types.TimeNow()
|
|
viewerID := now.UnixNano() & 0x3fffffffffffffff
|
|
targetID := viewerID + 1
|
|
viewer := store.EncodeUid(viewerID)
|
|
target := store.EncodeUid(targetID)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM im_friendships WHERE user_id IN (?,?) OR peer_id IN (?,?)", viewerID, targetID, viewerID, targetID)
|
|
db.Exec("DELETE FROM im_user_privacy_settings WHERE user_id IN (?,?)", viewerID, targetID)
|
|
db.Exec("DELETE FROM im_user_profiles WHERE user_id IN (?,?)", viewerID, targetID)
|
|
db.Exec("DELETE FROM im_credentials WHERE userid IN (?,?)", viewerID, targetID)
|
|
db.Exec("DELETE FROM im_auth WHERE userid IN (?,?)", viewerID, targetID)
|
|
db.Exec("DELETE FROM im_users WHERE id IN (?,?)", viewerID, targetID)
|
|
})
|
|
|
|
for _, user := range []struct {
|
|
id int64
|
|
username string
|
|
phone any
|
|
nickname string
|
|
}{
|
|
{viewerID, "viewerx", nil, "Viewer"},
|
|
{targetID, "potter", "+8615500000002", "Potter"},
|
|
} {
|
|
if _, err := db.Exec(
|
|
`INSERT INTO im_users(id,account_no,username,createdat,updatedat,state,access,phone,trusted)
|
|
VALUES(?,?,?,?,?,?,?,?,?)`,
|
|
user.id, user.id%1000000, user.username, now, now, types.StateOK, `{"Auth":"JRWPAS","Anon":"N"}`, user.phone, `{}`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if user.id != targetID {
|
|
if _, err := db.Exec(
|
|
`INSERT INTO im_user_profiles(id,user_id,nickname,public_account,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?)`,
|
|
user.id+2000, user.id, user.nickname, store.EncodeUid(user.id).UserId(), now, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := db.Exec(
|
|
`INSERT INTO im_user_privacy_settings(id,user_id,add_me_methods,created_at,updated_at)
|
|
VALUES(?,?,?,?,?)`,
|
|
user.id+1000, user.id, `{"account":true,"phone":true,"qr":true,"group":true}`, now, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := db.Exec(
|
|
`INSERT INTO im_user_profiles(id,user_id,nickname,public_account,avatar_file_id,signature,gender,region,created_at,updated_at)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
|
targetID+1000, targetID, "后端-potter", "15500000002", "avatar-1", "hi", "other", "CN", now, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
query string
|
|
searchType string
|
|
}{
|
|
{"username", "potter", "account"},
|
|
{"public_account", "15500000002", "account"},
|
|
{"phone_raw", "15500000002", "account"},
|
|
{"phone_e164", "+8615500000002", "phone"},
|
|
} {
|
|
users, _, err := businessAdp.TenantUserSearch(tenantID, viewer, tc.query, tc.searchType, "", 20)
|
|
if err != nil {
|
|
t.Fatalf("%s search failed: %v", tc.name, err)
|
|
}
|
|
if len(users) != 1 || users[0].Uid() != target {
|
|
t.Fatalf("%s search returned %+v, want %s", tc.name, users, target.UserId())
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================== Create tests ================================
|
|
func TestUserCreate(t *testing.T) {
|
|
for _, user := range testData.Users {
|
|
if err := adp.UserCreate(user); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
var count int
|
|
|
|
if err := db.Ping(); err != nil {
|
|
logs.Err.Println("Database ping failed:", err)
|
|
}
|
|
err := db.QueryRow("SELECT COUNT(*) FROM im_users").Scan(&count)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if count == 0 {
|
|
t.Error("No im_users created!")
|
|
}
|
|
}
|
|
|
|
func TestCredUpsert(t *testing.T) {
|
|
// Test just inserts:
|
|
for i := 0; i < 2; i++ {
|
|
inserted, err := adp.CredUpsert(testData.Creds[i])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !inserted {
|
|
t.Error("Should be inserted, but updated")
|
|
}
|
|
}
|
|
|
|
// Test duplicate:
|
|
_, err := adp.CredUpsert(testData.Creds[1])
|
|
if err != types.ErrDuplicate {
|
|
t.Error("Should return duplicate error but got", err)
|
|
}
|
|
_, err = adp.CredUpsert(testData.Creds[2])
|
|
if err != types.ErrDuplicate {
|
|
t.Error("Should return duplicate error but got", err)
|
|
}
|
|
|
|
// Test add new unvalidated credentials
|
|
inserted, err := adp.CredUpsert(testData.Creds[3])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !inserted {
|
|
t.Error("Should be inserted, but updated")
|
|
}
|
|
inserted, err = adp.CredUpsert(testData.Creds[3])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inserted {
|
|
t.Error("Should be updated, but inserted")
|
|
}
|
|
|
|
// Just insert other creds (used in other tests)
|
|
for _, cred := range testData.Creds[4:] {
|
|
_, err = adp.CredUpsert(cred)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuthAddRecord(t *testing.T) {
|
|
for _, rec := range testData.Recs {
|
|
err := adp.AuthAddRecord(types.ParseUserId("usr"+rec.UserId), rec.Scheme, strings.TrimPrefix(rec.Unique, "basic:"),
|
|
rec.AuthLvl, rec.Secret, rec.Expires)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
//Test duplicate
|
|
err := adp.AuthAddRecord(types.ParseUserId("usr"+testData.Users[0].Id), testData.Recs[0].Scheme,
|
|
strings.TrimPrefix(testData.Recs[0].Unique, "basic:"), testData.Recs[0].AuthLvl, testData.Recs[0].Secret, testData.Recs[0].Expires)
|
|
if err != types.ErrDuplicate {
|
|
t.Fatal("Should be duplicate error but got", err)
|
|
}
|
|
}
|
|
|
|
func TestRegistrationInviteLifecycle(t *testing.T) {
|
|
inviteAdapter, ok := adp.(adapter.TenantRegistrationInviteAdapter)
|
|
if !ok {
|
|
t.Fatal("MySQL adapter does not implement TenantRegistrationInviteAdapter")
|
|
}
|
|
var tenantID types.TenantID
|
|
if err := db.Get(&tenantID, "SELECT id FROM im_tenant WHERE code='default'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := types.TimeNow()
|
|
invite := &types.RegistrationInviteCode{
|
|
Code: "AbCdE",
|
|
ExpiresAt: now.Add(time.Hour),
|
|
CreatedBy: 10001,
|
|
Remark: "integration test",
|
|
}
|
|
if err := inviteAdapter.TenantRegistrationInviteCreate(tenantID, invite); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, err := inviteAdapter.TenantRegistrationInviteGetByCode(tenantID, "AbCdE")
|
|
if err != nil || stored.Code != "AbCdE" || stored.Status != types.RegistrationInviteActive {
|
|
t.Fatalf("stored invite=%#v err=%v", stored, err)
|
|
}
|
|
if _, err = inviteAdapter.TenantRegistrationInviteGetByCode(tenantID, "abcde"); err != types.ErrInviteInvalid {
|
|
t.Fatalf("case-insensitive invite lookup was accepted: %v", err)
|
|
}
|
|
firstUID := types.ParseUserId("usr" + testData.Users[0].Id)
|
|
if err = inviteAdapter.TenantRegistrationInviteRecordUse(tenantID, "AbCdE", firstUID, "req-invite-test", now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondUID := types.ParseUserId("usr" + testData.Users[1].Id)
|
|
if err = inviteAdapter.TenantRegistrationInviteRecordUse(tenantID, "AbCdE", secondUID, "req-invite-second-user", now); err != nil {
|
|
t.Fatalf("second user could not reuse invite: %v", err)
|
|
}
|
|
if err = inviteAdapter.TenantRegistrationInviteSyncUsage(tenantID, invite.ID); err != nil {
|
|
t.Fatalf("invite usage projection failed: %v", err)
|
|
}
|
|
stored, err = inviteAdapter.TenantRegistrationInviteGetByCode(tenantID, "AbCdE")
|
|
if err != nil || stored.Status != types.RegistrationInviteActive || stored.UsageCount != 2 {
|
|
t.Fatalf("reusable invite=%#v err=%v", stored, err)
|
|
}
|
|
}
|
|
|
|
func TestRegistrationInviteReusableSchema(t *testing.T) {
|
|
var count int
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_registration_invite_codes' AND column_name IN ('used_by','used_at')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("single-use columns still exist: %d", count)
|
|
}
|
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.statistics
|
|
WHERE table_schema=DATABASE() AND table_name='im_registration_invite_uses'
|
|
AND index_name='idx_im_registration_invite_use_code' AND non_unique=1`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("reusable invite use index count: got %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestVerificationTargetUserRoundTrip(t *testing.T) {
|
|
tenantAdapter, ok := adp.(adapter.TenantBusinessAdapter)
|
|
if !ok {
|
|
t.Fatal("MySQL adapter does not implement TenantBusinessAdapter")
|
|
}
|
|
var tenantID types.TenantID
|
|
if err := db.Get(&tenantID, "SELECT id FROM im_tenant WHERE code='default'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
uid := types.ParseUserId("usr" + testData.Users[0].Id)
|
|
now := types.TimeNow()
|
|
challengeID := "challenge-roundtrip-target-user"
|
|
phoneHash := "verification-target-user-phone-hash"
|
|
_, _ = db.Exec("DELETE FROM im_verification_codes WHERE challenge_id=? OR phone_hash=?", challengeID, phoneHash)
|
|
vc := &types.VerificationCode{
|
|
TenantID: tenantID,
|
|
ChallengeID: challengeID,
|
|
Scene: "reset_password",
|
|
PhoneHash: phoneHash,
|
|
PhoneMask: "+86******0000",
|
|
TargetUser: uid,
|
|
CodeHash: strings.Repeat("a", 64),
|
|
State: types.VerificationStatePending,
|
|
MaxAttempts: 5,
|
|
ExpiresAt: now.Add(time.Hour),
|
|
ResendAfter: now.Add(time.Minute),
|
|
}
|
|
if err := tenantAdapter.TenantVerificationCreate(tenantID, vc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stored, err := tenantAdapter.TenantVerificationGet(tenantID, challengeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.TargetUser != uid {
|
|
t.Fatalf("Get target user: got %s, want %s", stored.TargetUser.UserId(), uid.UserId())
|
|
}
|
|
|
|
active, err := tenantAdapter.TenantVerificationGetActive(tenantID, "reset_password", phoneHash, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if active.TargetUser != uid {
|
|
t.Fatalf("GetActive target user: got %s, want %s", active.TargetUser.UserId(), uid.UserId())
|
|
}
|
|
}
|
|
|
|
func TestTopicCreate(t *testing.T) {
|
|
err := adp.TopicCreate(testData.Topics[0])
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
for _, tpc := range testData.Topics[3:] {
|
|
err = adp.TopicCreate(tpc)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func decodeUid(u string) int64 {
|
|
return store.DecodeUid(types.ParseUid(u))
|
|
}
|
|
|
|
func TestTopicCreateP2P(t *testing.T) {
|
|
err := adp.TopicCreateP2P(testData.Subs[2], testData.Subs[3])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldModeGiven := testData.Subs[2].ModeGiven
|
|
testData.Subs[2].ModeGiven = 255
|
|
err = adp.TopicCreateP2P(testData.Subs[4], testData.Subs[2])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var got types.Subscription
|
|
err = db.QueryRow("SELECT createdat,updatedat,deletedat,userid,topic,delid,recvseqid,readseqid,modewant,modegiven,private FROM im_subscriptions WHERE topic=? AND userid=?",
|
|
testData.Subs[2].Topic, decodeUid(testData.Subs[2].User)).Scan(&got.CreatedAt,
|
|
&got.UpdatedAt, &got.DeletedAt, &got.User, &got.Topic, &got.DelId, &got.RecvSeqId, &got.ReadSeqId, &got.ModeWant, &got.ModeGiven, &got.Private)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.ModeGiven == oldModeGiven {
|
|
t.Error("ModeGiven update failed")
|
|
}
|
|
}
|
|
|
|
func TestTenantTopicCreateP2PWritesTenantSubscriptions(t *testing.T) {
|
|
tenantAdapter, ok := adp.(adapter.TenantBusinessAdapter)
|
|
if !ok {
|
|
t.Fatal("MySQL adapter does not implement TenantBusinessAdapter")
|
|
}
|
|
var tenantID types.TenantID
|
|
if err := db.Get(&tenantID, "SELECT id FROM im_tenant WHERE code='default'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var tenantColumns int
|
|
if err := db.Get(&tenantColumns, `SELECT COUNT(*) FROM information_schema.columns
|
|
WHERE table_schema=DATABASE() AND table_name='im_subscriptions' AND column_name='tenant_id'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tenantColumns == 0 {
|
|
t.Skip("im_subscriptions.tenant_id is missing in the current test database")
|
|
}
|
|
|
|
first := types.ParseUserId("usr" + testData.Users[0].Id)
|
|
second := types.ParseUserId("usr" + testData.Users[2].Id)
|
|
topic := first.P2PName(second)
|
|
if _, err := db.Exec("DELETE FROM im_subscriptions WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.Exec("DELETE FROM im_topics WHERE tenant_id=? AND name=?", tenantID, topic); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
now := types.TimeNow()
|
|
one := &types.Subscription{
|
|
TenantID: tenantID,
|
|
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
|
|
User: first.String(),
|
|
Topic: topic,
|
|
ModeWant: types.ModeCPublic,
|
|
ModeGiven: types.ModeCPublic,
|
|
}
|
|
one.SetTouchedAt(now)
|
|
two := &types.Subscription{
|
|
TenantID: tenantID,
|
|
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
|
|
User: second.String(),
|
|
Topic: topic,
|
|
ModeWant: types.ModeCPublic,
|
|
ModeGiven: types.ModeCPublic,
|
|
}
|
|
two.SetTouchedAt(now)
|
|
|
|
if err := tenantAdapter.TenantTopicCreateP2P(tenantID, one, two); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var count int
|
|
if err := db.Get(&count, "SELECT COUNT(*) FROM im_subscriptions WHERE tenant_id=? AND topic=?", tenantID, topic); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("tenant P2P subscriptions: got %d, want 2", count)
|
|
}
|
|
subs, err := tenantAdapter.TenantUsersForTopic(tenantID, topic, false, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(subs) != 2 {
|
|
t.Fatalf("TenantUsersForTopic returned %d subscriptions, want 2", len(subs))
|
|
}
|
|
}
|
|
|
|
func TestTopicShare(t *testing.T) {
|
|
if err := adp.TopicShare(testData.Subs[0].Topic, testData.Subs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Must save recvseqid and readseqid separately because TopicShare
|
|
// ignores them.
|
|
for _, sub := range testData.Subs {
|
|
adp.SubsUpdate(sub.Topic, types.ParseUid(sub.User), map[string]any{
|
|
"delid": sub.DelId,
|
|
"recvseqid": sub.RecvSeqId,
|
|
"readseqid": sub.ReadSeqId,
|
|
})
|
|
}
|
|
|
|
// Update topic SeqId because it's not saved at creation time but used by the tests.
|
|
for _, tpc := range testData.Topics {
|
|
err := adp.TopicUpdate(tpc.Id, map[string]any{
|
|
"seqid": tpc.SeqId,
|
|
"delid": tpc.DelId,
|
|
})
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMessageSave(t *testing.T) {
|
|
for _, msg := range testData.Msgs {
|
|
err := adp.MessageSave(msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Some messages are soft deleted, but it's ignored by adp.MessageSave
|
|
for _, msg := range testData.Msgs {
|
|
if len(msg.DeletedFor) > 0 {
|
|
for _, del := range msg.DeletedFor {
|
|
toDel := types.DelMessage{
|
|
Topic: msg.Topic,
|
|
DeletedFor: del.User,
|
|
DelId: del.DelId,
|
|
SeqIdRanges: []types.Range{{Low: msg.SeqId}},
|
|
}
|
|
adp.MessageDeleteList(msg.Topic, &toDel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFileStartUpload(t *testing.T) {
|
|
for _, f := range testData.Files {
|
|
err := adp.FileStartUpload(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================== Read tests ==================================
|
|
func TestUserGet(t *testing.T) {
|
|
// Test not found
|
|
got, err := adp.UserGet(dummyUid1)
|
|
if err == nil && got != nil {
|
|
t.Error("user should be nil.")
|
|
}
|
|
|
|
got, err = adp.UserGet(types.ParseUserId("usr" + testData.Users[0].Id))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// User agent is not stored when creating a user. Make sure it's the same.
|
|
got.UserAgent = testData.Users[0].UserAgent
|
|
got.CreatedAt = testData.Users[0].CreatedAt
|
|
got.UpdatedAt = testData.Users[0].UpdatedAt
|
|
|
|
if !reflect.DeepEqual(got, testData.Users[0]) {
|
|
t.Error(mismatchErrorString("User", got, testData.Users[0]))
|
|
}
|
|
}
|
|
|
|
func TestUserGetAll(t *testing.T) {
|
|
// Test not found (dummy UIDs).
|
|
got, err := adp.UserGetAll(dummyUid1, dummyUid2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) > 0 {
|
|
t.Error("result im_users should be zero length, got", len(got))
|
|
}
|
|
|
|
got, err = adp.UserGetAll(types.ParseUserId("usr"+testData.Users[0].Id), types.ParseUserId("usr"+testData.Users[1].Id))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatal(mismatchErrorString("resultUsers length", len(got), 2))
|
|
}
|
|
for i, usr := range got {
|
|
// User agent is not compared.
|
|
usr.UserAgent = testData.Users[i].UserAgent
|
|
usr.CreatedAt = testData.Users[i].CreatedAt
|
|
usr.UpdatedAt = testData.Users[i].UpdatedAt
|
|
if !reflect.DeepEqual(&usr, testData.Users[i]) {
|
|
t.Error(mismatchErrorString("User", &usr, testData.Users[i]))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUserGetByCred(t *testing.T) {
|
|
// Test not found
|
|
got, err := adp.UserGetByCred("foo", "bar")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != types.ZeroUid {
|
|
t.Error("result uid should be ZeroUid")
|
|
}
|
|
|
|
got, _ = adp.UserGetByCred(testData.Creds[0].Method, testData.Creds[0].Value)
|
|
if got != types.ParseUserId("usr"+testData.Creds[0].User) {
|
|
t.Error(mismatchErrorString("Uid", got, types.ParseUserId("usr"+testData.Creds[0].User)))
|
|
}
|
|
}
|
|
|
|
func TestCredGetActive(t *testing.T) {
|
|
got, err := adp.CredGetActive(types.ParseUserId("usr"+testData.Users[2].Id), "tel")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if !reflect.DeepEqual(got, testData.Creds[3]) {
|
|
t.Error(mismatchErrorString("Credential", got, testData.Creds[3]))
|
|
}
|
|
|
|
// Test not found
|
|
got, err = adp.CredGetActive(dummyUid1, "")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if got != nil {
|
|
t.Error("result should be nil, but got", got)
|
|
}
|
|
}
|
|
|
|
func TestCredGetAll(t *testing.T) {
|
|
got, err := adp.CredGetAll(types.ParseUserId("usr"+testData.Users[2].Id), "", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 3 {
|
|
t.Error(mismatchErrorString("Credentials length", len(got), 3))
|
|
}
|
|
|
|
got, _ = adp.CredGetAll(types.ParseUserId("usr"+testData.Users[2].Id), "tel", false)
|
|
if len(got) != 2 {
|
|
t.Error(mismatchErrorString("Credentials length", len(got), 2))
|
|
}
|
|
|
|
got, _ = adp.CredGetAll(types.ParseUserId("usr"+testData.Users[2].Id), "", true)
|
|
if len(got) != 1 {
|
|
t.Error(mismatchErrorString("Credentials length", len(got), 1))
|
|
}
|
|
|
|
got, _ = adp.CredGetAll(types.ParseUserId("usr"+testData.Users[2].Id), "tel", true)
|
|
if len(got) != 1 {
|
|
t.Error(mismatchErrorString("Credentials length", len(got), 1))
|
|
}
|
|
}
|
|
|
|
func TestAuthGetUniqueRecord(t *testing.T) {
|
|
uid, authLvl, secret, expires, err := adp.AuthGetUniqueRecord("alice")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if uid != types.ParseUserId("usr"+testData.Recs[0].UserId) ||
|
|
authLvl != testData.Recs[0].AuthLvl ||
|
|
!reflect.DeepEqual(secret, testData.Recs[0].Secret) ||
|
|
expires != testData.Recs[0].Expires {
|
|
|
|
got := fmt.Sprintf("%v %v %v %v", uid, authLvl, secret, expires)
|
|
want := fmt.Sprintf("%v %v %v %v", testData.Recs[0].UserId, testData.Recs[0].AuthLvl, testData.Recs[0].Secret, testData.Recs[0].Expires)
|
|
t.Error(mismatchErrorString("Auth record", got, want))
|
|
}
|
|
|
|
// Test not found
|
|
uid, _, _, _, err = adp.AuthGetUniqueRecord("qwert:asdfg")
|
|
if err == nil && !uid.IsZero() {
|
|
t.Error("Auth record found but shouldn't. Uid:", uid.String())
|
|
}
|
|
}
|
|
|
|
func TestAuthGetRecord(t *testing.T) {
|
|
recId, authLvl, secret, expires, err := adp.AuthGetRecord(types.ParseUserId("usr"+testData.Recs[0].UserId), "basic")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantLogin := strings.TrimPrefix(testData.Recs[0].Unique, "basic:")
|
|
if recId != wantLogin ||
|
|
authLvl != testData.Recs[0].AuthLvl ||
|
|
!reflect.DeepEqual(secret, testData.Recs[0].Secret) ||
|
|
expires != testData.Recs[0].Expires {
|
|
|
|
got := fmt.Sprintf("%v %v %v %v", recId, authLvl, secret, expires)
|
|
want := fmt.Sprintf("%v %v %v %v", wantLogin, testData.Recs[0].AuthLvl, testData.Recs[0].Secret, testData.Recs[0].Expires)
|
|
t.Error(mismatchErrorString("Auth record", got, want))
|
|
}
|
|
|
|
// Test not found
|
|
recId, _, _, _, err = adp.AuthGetRecord(types.Uid(123), "scheme")
|
|
if err != types.ErrNotFound {
|
|
t.Error("Auth record found but shouldn't. recId:", recId)
|
|
}
|
|
}
|
|
|
|
func TestTopicGet(t *testing.T) {
|
|
got, err := adp.TopicGet(testData.Topics[0].Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got, testData.Topics[0]) {
|
|
t.Error(mismatchErrorString("Topic", got, testData.Topics[0]))
|
|
}
|
|
// Test not found
|
|
got, err = adp.TopicGet("asdfasdfasdf")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != nil {
|
|
t.Error("Topic should be nil but got:", got)
|
|
}
|
|
}
|
|
|
|
func TestTopicsForUser(t *testing.T) {
|
|
qOpts := types.QueryOpt{
|
|
Topic: "p2p9AVDamaNCRbfKzGSh3mE0w",
|
|
Limit: 999,
|
|
}
|
|
gotSubs, err := adp.TopicsForUser(types.ParseUserId("usr"+testData.Users[0].Id), false, &qOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 1 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 1))
|
|
}
|
|
|
|
gotSubs, err = adp.TopicsForUser(types.ParseUserId("usr"+testData.Users[1].Id), true, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 2 {
|
|
t.Error(mismatchErrorString("Subs length (2)", len(gotSubs), 2))
|
|
}
|
|
|
|
qOpts.Topic = ""
|
|
ims := testData.Now.Add(15 * time.Minute)
|
|
qOpts.IfModifiedSince = &ims
|
|
gotSubs, err = adp.TopicsForUser(types.ParseUserId("usr"+testData.Users[0].Id), false, &qOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 1 {
|
|
t.Error(mismatchErrorString("Subs length (IMS)", len(gotSubs), 1))
|
|
}
|
|
|
|
ims = time.Now().Add(15 * time.Minute)
|
|
gotSubs, err = adp.TopicsForUser(types.ParseUserId("usr"+testData.Users[0].Id), false, &qOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 0 {
|
|
t.Error(mismatchErrorString("Subs length (IMS 2)", len(gotSubs), 0))
|
|
}
|
|
}
|
|
|
|
func TestUsersForTopic(t *testing.T) {
|
|
qOpts := types.QueryOpt{
|
|
User: types.ParseUserId("usr" + testData.Users[0].Id),
|
|
Limit: 999,
|
|
}
|
|
gotSubs, err := adp.UsersForTopic("grpgRXf0rU4uR4", false, &qOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 1 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 1))
|
|
}
|
|
|
|
gotSubs, err = adp.UsersForTopic("grpgRXf0rU4uR4", true, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 2 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 2))
|
|
}
|
|
|
|
gotSubs, err = adp.UsersForTopic("p2p9AVDamaNCRbfKzGSh3mE0w", false, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 2 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 2))
|
|
}
|
|
}
|
|
|
|
func TestOwnTopics(t *testing.T) {
|
|
gotSubs, err := adp.OwnTopics(types.ParseUserId("usr" + testData.Users[0].Id))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotSubs) != 1 {
|
|
t.Fatalf("Got topic length %v instead of %v", len(gotSubs), 1)
|
|
}
|
|
if gotSubs[0] != testData.Topics[0].Id {
|
|
t.Errorf("Got topic %v instead of %v", gotSubs[0], testData.Topics[0].Id)
|
|
}
|
|
}
|
|
|
|
func TestSubscriptionGet(t *testing.T) {
|
|
got, err := adp.SubscriptionGet(testData.Topics[0].Id, types.ParseUserId("usr"+testData.Users[0].Id), false)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
if diff := cmp.Diff(got, testData.Subs[0],
|
|
cmpopts.IgnoreUnexported(types.Subscription{}, types.ObjHeader{})); diff != "" {
|
|
t.Error(mismatchErrorString("Subs", diff, ""))
|
|
}
|
|
// Test not found
|
|
got, err = adp.SubscriptionGet("dummytopic", dummyUid1, false)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if got != nil {
|
|
t.Error("result sub should be nil.")
|
|
}
|
|
}
|
|
|
|
func TestSubsForUser(t *testing.T) {
|
|
gotSubs, err := adp.SubsForUser(types.ParseUserId("usr" + testData.Users[0].Id))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if len(gotSubs) != 2 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 1))
|
|
}
|
|
|
|
// Test not found
|
|
gotSubs, err = adp.SubsForUser(types.ParseUserId("usr12345678"))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if len(gotSubs) != 0 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 0))
|
|
}
|
|
}
|
|
|
|
func TestSubsForTopic(t *testing.T) {
|
|
qOpts := types.QueryOpt{
|
|
User: types.ParseUserId("usr" + testData.Users[0].Id),
|
|
Limit: 999,
|
|
}
|
|
gotSubs, err := adp.SubsForTopic(testData.Topics[0].Id, false, &qOpts)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if len(gotSubs) != 1 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 1))
|
|
}
|
|
// Test not found
|
|
gotSubs, err = adp.SubsForTopic("dummytopicid", false, nil)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if len(gotSubs) != 0 {
|
|
t.Error(mismatchErrorString("Subs length", len(gotSubs), 0))
|
|
}
|
|
}
|
|
|
|
func TestFind(t *testing.T) {
|
|
reqTags := [][]string{{"alice", "bob", "carol", "travel", "qwer", "asdf", "zxcv"}}
|
|
got, err := adp.Find("usr"+testData.Users[2].Id, "", reqTags, nil, true)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Error(mismatchErrorString("topic-only result length", len(got), 1))
|
|
}
|
|
}
|
|
|
|
func TestMessageGetAll(t *testing.T) {
|
|
opts := types.QueryOpt{
|
|
Since: 1,
|
|
Before: 2,
|
|
Limit: 999,
|
|
}
|
|
gotMsgs, err := adp.MessageGetAll(testData.Topics[0].Id, types.ParseUserId("usr"+testData.Users[0].Id), &opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(gotMsgs) != 1 {
|
|
t.Error(mismatchErrorString("Messages length opts", len(gotMsgs), 1))
|
|
}
|
|
gotMsgs, _ = adp.MessageGetAll(testData.Topics[0].Id, types.ParseUserId("usr"+testData.Users[0].Id), nil)
|
|
if len(gotMsgs) != 2 {
|
|
t.Fatalf("%+v", gotMsgs)
|
|
t.Error(mismatchErrorString("Messages length no opts", len(gotMsgs), 2))
|
|
}
|
|
gotMsgs, _ = adp.MessageGetAll(testData.Topics[0].Id, types.ZeroUid, nil)
|
|
if len(gotMsgs) != 3 {
|
|
t.Error(mismatchErrorString("Messages length zero uid", len(gotMsgs), 3))
|
|
}
|
|
}
|
|
|
|
func TestFileGet(t *testing.T) {
|
|
// General test done during TestFileFinishUpload().
|
|
|
|
// Test not found
|
|
got, err := adp.FileGet("dummyfileid")
|
|
if err != nil {
|
|
if got != nil {
|
|
t.Error("File found but shouldn't:", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ================== Update tests ================================
|
|
func TestUserUpdate(t *testing.T) {
|
|
update := map[string]any{
|
|
"UserAgent": "Test Agent v0.11",
|
|
"UpdatedAt": testData.Now.Add(30 * time.Minute),
|
|
}
|
|
err := adp.UserUpdate(types.ParseUserId("usr"+testData.Users[0].Id), update)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var got struct {
|
|
UserAgent string
|
|
UpdatedAt time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
err = db.QueryRow("SELECT useragent, updatedat, createdat FROM im_users WHERE id=?", decodeUid(testData.Users[0].Id)).
|
|
Scan(&got.UserAgent, &got.UpdatedAt, &got.CreatedAt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.UserAgent != "Test Agent v0.11" {
|
|
t.Error(mismatchErrorString("UserAgent", got.UserAgent, "Test Agent v0.11"))
|
|
}
|
|
if got.UpdatedAt.Equal(got.CreatedAt) {
|
|
t.Error("UpdatedAt field not updated")
|
|
}
|
|
}
|
|
|
|
func TestUserUpdateTags(t *testing.T) {
|
|
uid := types.ParseUserId("usr" + testData.Users[0].Id)
|
|
got, err := adp.UserUpdateTags(uid, []string{"legacy"}, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("user tags must stay disabled, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestCredFail(t *testing.T) {
|
|
err := adp.CredFail(types.ParseUserId("usr"+testData.Creds[3].User), "tel")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// Check if fields updated
|
|
var got struct {
|
|
Retries int
|
|
UpdatedAt time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
err = db.QueryRow("SELECT retries, updatedat, createdat FROM im_credentials WHERE userid=? AND method=? AND value=?",
|
|
decodeUid(testData.Creds[3].User), "tel", testData.Creds[3].Value).Scan(&got.Retries, &got.UpdatedAt, &got.CreatedAt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Retries != 1 {
|
|
t.Error(mismatchErrorString("Retries count", got.Retries, 1))
|
|
}
|
|
if got.UpdatedAt.Equal(got.CreatedAt) {
|
|
t.Error("UpdatedAt field not updated")
|
|
}
|
|
}
|
|
|
|
func TestCredConfirm(t *testing.T) {
|
|
err := adp.CredConfirm(types.ParseUserId("usr"+testData.Creds[3].User), "tel")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test fields are updated
|
|
var got struct {
|
|
UpdatedAt time.Time
|
|
CreatedAt time.Time
|
|
Done bool
|
|
}
|
|
err = db.QueryRow("SELECT updatedat, createdat, done FROM im_credentials WHERE userid=? AND method=? AND value=?",
|
|
decodeUid(testData.Creds[3].User), "tel", testData.Creds[3].Value).Scan(&got.UpdatedAt, &got.CreatedAt, &got.Done)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.UpdatedAt.Equal(got.CreatedAt) {
|
|
t.Error("Credential not updated correctly")
|
|
}
|
|
if !got.Done {
|
|
t.Error("Credential should be marked as done")
|
|
}
|
|
}
|
|
|
|
func TestAuthUpdRecord(t *testing.T) {
|
|
rec := testData.Recs[1]
|
|
newSecret := []byte{'s', 'e', 'c', 'r', 'e', 't'}
|
|
err := adp.AuthUpdRecord(types.ParseUserId("usr"+rec.UserId), rec.Scheme, strings.TrimPrefix(rec.Unique, "basic:"),
|
|
rec.AuthLvl, newSecret, rec.Expires)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got []byte
|
|
err = db.QueryRow("SELECT secret FROM im_auth WHERE userid=?", decodeUid(rec.UserId)).Scan(&got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reflect.DeepEqual(got, rec.Secret) {
|
|
t.Error(mismatchErrorString("Secret", got, rec.Secret))
|
|
}
|
|
|
|
// Test with auth ID (unique) change
|
|
newId := "bob12345"
|
|
err = adp.AuthUpdRecord(types.ParseUserId("usr"+rec.UserId), rec.Scheme, newId,
|
|
rec.AuthLvl, newSecret, rec.Expires)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Test if old ID deleted
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_users WHERE id=? AND username=?", decodeUid(rec.UserId), strings.TrimPrefix(rec.Unique, "basic:")).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Old im_auth record not deleted")
|
|
}
|
|
}
|
|
|
|
func TestTopicUpdateOnMessage(t *testing.T) {
|
|
msg := types.Message{
|
|
ObjHeader: types.ObjHeader{
|
|
CreatedAt: testData.Now.Add(33 * time.Minute),
|
|
},
|
|
SeqId: 66,
|
|
}
|
|
err := adp.TopicUpdateOnMessage(testData.Topics[2].Id, &msg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got struct {
|
|
TouchedAt time.Time
|
|
SeqId int64
|
|
}
|
|
err = db.QueryRow("SELECT touchedat, seqid FROM im_topics WHERE name=?", testData.Topics[2].Id).
|
|
Scan(&got.TouchedAt, &got.SeqId)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.TouchedAt != msg.CreatedAt || got.SeqId != msg.SeqId {
|
|
t.Error(mismatchErrorString("TouchedAt", got.TouchedAt, msg.CreatedAt))
|
|
t.Error(mismatchErrorString("SeqId", got.SeqId, msg.SeqId))
|
|
}
|
|
}
|
|
|
|
func TestTopicUpdate(t *testing.T) {
|
|
update := map[string]any{
|
|
"UpdatedAt": testData.Now.Add(55 * time.Minute),
|
|
}
|
|
err := adp.TopicUpdate(testData.Topics[0].Id, update)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got time.Time
|
|
err = db.QueryRow("SELECT updatedat FROM im_topics WHERE name=?", testData.Topics[0].Id).Scan(&got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != update["UpdatedAt"] {
|
|
t.Error(mismatchErrorString("UpdatedAt", got, update["UpdatedAt"]))
|
|
}
|
|
}
|
|
|
|
func TestTopicOwnerChange(t *testing.T) {
|
|
err := adp.TopicOwnerChange(testData.Topics[0].Id, types.ParseUserId("usr"+testData.Users[1].Id))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got int64
|
|
err = db.QueryRow("SELECT owner FROM im_topics WHERE name=?", testData.Topics[0].Id).Scan(&got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expectedOwner := decodeUid(testData.Users[1].Id) // Assuming user ID conversion
|
|
if got != expectedOwner {
|
|
t.Error(mismatchErrorString("Owner", got, expectedOwner))
|
|
}
|
|
}
|
|
|
|
func TestSubsUpdate(t *testing.T) {
|
|
update := map[string]any{
|
|
"UpdatedAt": testData.Now.Add(22 * time.Minute),
|
|
}
|
|
err := adp.SubsUpdate(testData.Topics[0].Id, types.ParseUserId("usr"+testData.Users[0].Id), update)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got time.Time
|
|
err = db.QueryRow("SELECT updatedat FROM im_subscriptions WHERE topic=? AND userid=?",
|
|
testData.Topics[0].Id, decodeUid(testData.Users[0].Id)).Scan(&got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != update["UpdatedAt"] {
|
|
t.Error(mismatchErrorString("UpdatedAt", got, update["UpdatedAt"]))
|
|
}
|
|
|
|
err = adp.SubsUpdate(testData.Topics[1].Id, types.ZeroUid, update)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.QueryRow("SELECT updatedat FROM im_subscriptions WHERE topic=? LIMIT 1",
|
|
testData.Topics[1].Id).Scan(&got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != update["UpdatedAt"] {
|
|
t.Error(mismatchErrorString("UpdatedAt", got, update["UpdatedAt"]))
|
|
}
|
|
}
|
|
|
|
func TestSubsDelete(t *testing.T) {
|
|
err := adp.SubsDelete(testData.Topics[1].Id, types.ParseUserId("usr"+testData.Users[0].Id))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var deletedat sql.NullTime
|
|
err = db.QueryRow("SELECT deletedat FROM im_subscriptions WHERE topic=? AND userid=?",
|
|
testData.Topics[1].Id, decodeUid(testData.Users[0].Id)).Scan(&deletedat)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !deletedat.Valid {
|
|
t.Error("DeletedAt should not be null")
|
|
}
|
|
}
|
|
|
|
func TestDeviceUpsert(t *testing.T) {
|
|
err := adp.DeviceUpsert(types.ParseUserId("usr"+testData.Users[0].Id), testData.Devs[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got struct {
|
|
DeviceId string
|
|
Platform string
|
|
}
|
|
err = db.QueryRow("SELECT deviceid, platform FROM im_devices WHERE userid=? LIMIT 1",
|
|
decodeUid(testData.Users[0].Id)).Scan(&got.DeviceId, &got.Platform)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.DeviceId != testData.Devs[0].DeviceId || got.Platform != testData.Devs[0].Platform {
|
|
t.Error(mismatchErrorString("Device", got, testData.Devs[0]))
|
|
}
|
|
|
|
// Test update
|
|
testData.Devs[0].Platform = "Web"
|
|
err = adp.DeviceUpsert(types.ParseUserId("usr"+testData.Users[0].Id), testData.Devs[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.QueryRow("SELECT platform FROM im_devices WHERE userid=? AND deviceid=?",
|
|
decodeUid(testData.Users[0].Id), testData.Devs[0].DeviceId).Scan(&got.Platform)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Platform != "Web" {
|
|
t.Error("Device not updated.", got.Platform)
|
|
}
|
|
|
|
// Test add same device to another user
|
|
err = adp.DeviceUpsert(types.ParseUserId("usr"+testData.Users[1].Id), testData.Devs[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.QueryRow("SELECT platform FROM im_devices WHERE userid=? AND deviceid=?",
|
|
decodeUid(testData.Users[1].Id), testData.Devs[0].DeviceId).Scan(&got.Platform)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Platform != "Web" {
|
|
t.Error("Device not updated.", got.Platform)
|
|
}
|
|
|
|
err = adp.DeviceUpsert(types.ParseUserId("usr"+testData.Users[2].Id), testData.Devs[1])
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestFileFinishUpload(t *testing.T) {
|
|
got, err := adp.FileFinishUpload(testData.Files[0], true, 22222)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Status != types.UploadCompleted {
|
|
t.Error(mismatchErrorString("Status", got.Status, types.UploadCompleted))
|
|
}
|
|
if got.Size != 22222 {
|
|
t.Error(mismatchErrorString("Size", got.Size, 22222))
|
|
}
|
|
}
|
|
|
|
func TestMessageAttachments(t *testing.T) {
|
|
fids := []string{testData.Files[0].Id, testData.Files[1].Id}
|
|
err := adp.FileLinkAttachments("", types.ZeroUid, types.ParseUid(testData.Msgs[1].Id), fids)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Check if attachments were linked (this would require checking filemsglinks table)
|
|
var count int
|
|
if err = db.QueryRow("SELECT COUNT(*) FROM im_filemsglinks WHERE msgid=?",
|
|
types.ParseUid(testData.Msgs[1].Id)).Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if count != len(fids) {
|
|
t.Error(mismatchErrorString("Attachments count", count, len(fids)))
|
|
}
|
|
}
|
|
|
|
// ================== Other tests =================================
|
|
func TestDeviceGetAll(t *testing.T) {
|
|
uid0 := types.ParseUserId("usr" + testData.Users[0].Id)
|
|
uid1 := types.ParseUserId("usr" + testData.Users[1].Id)
|
|
uid2 := types.ParseUserId("usr" + testData.Users[2].Id)
|
|
gotDevs, count, err := adp.DeviceGetAll(uid0, uid1, uid2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 2 {
|
|
t.Fatal(mismatchErrorString("count", count, 2))
|
|
}
|
|
if !reflect.DeepEqual(gotDevs[uid1][0], *testData.Devs[0]) {
|
|
t.Error(mismatchErrorString("Device", gotDevs[uid1][0], *testData.Devs[0]))
|
|
}
|
|
if !reflect.DeepEqual(gotDevs[uid2][0], *testData.Devs[1]) {
|
|
t.Error(mismatchErrorString("Device", gotDevs[uid2][0], *testData.Devs[1]))
|
|
}
|
|
}
|
|
|
|
func TestDeviceDelete(t *testing.T) {
|
|
err := adp.DeviceDelete(types.ParseUserId("usr"+testData.Users[1].Id), testData.Devs[0].DeviceId)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_devices WHERE userid=?", testData.Users[1].Id).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Device not deleted:", count)
|
|
}
|
|
|
|
err = adp.DeviceDelete(types.ParseUserId("usr"+testData.Users[2].Id), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_devices WHERE userid=?", testData.Users[2].Id).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Device not deleted:", count)
|
|
}
|
|
}
|
|
|
|
// ================== Persistent Cache tests ======================
|
|
func TestPCacheUpsert(t *testing.T) {
|
|
err := adp.PCacheUpsert("test_key", "test_value", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test duplicate with failOnDuplicate = true
|
|
err = adp.PCacheUpsert("test_key2", "test_value2", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = adp.PCacheUpsert("test_key2", "new_value", true)
|
|
if err != types.ErrDuplicate {
|
|
t.Error("Expected duplicate error")
|
|
}
|
|
}
|
|
|
|
func TestPCacheGet(t *testing.T) {
|
|
value, err := adp.PCacheGet("test_key")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value != "test_value" {
|
|
t.Error(mismatchErrorString("Cache value", value, "test_value"))
|
|
}
|
|
|
|
// Test not found
|
|
_, err = adp.PCacheGet("nonexistent")
|
|
if err != types.ErrNotFound {
|
|
t.Error("Expected not found error")
|
|
}
|
|
}
|
|
|
|
func TestPCacheDelete(t *testing.T) {
|
|
err := adp.PCacheDelete("test_key")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify deleted
|
|
_, err = adp.PCacheGet("test_key")
|
|
if err != types.ErrNotFound {
|
|
t.Error("Key should be deleted")
|
|
}
|
|
}
|
|
|
|
func TestPCacheExpire(t *testing.T) {
|
|
// Insert some test keys with prefix
|
|
adp.PCacheUpsert("prefix_key1", "value1", false)
|
|
adp.PCacheUpsert("prefix_key2", "value2", false)
|
|
|
|
// Expire keys older than now (should delete all test keys)
|
|
err := adp.PCacheExpire("prefix_", time.Now().Add(1*time.Minute))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// ================== Delete tests ================================
|
|
func TestCredDel(t *testing.T) {
|
|
err := adp.CredDel(types.ParseUserId("usr"+testData.Users[0].Id), "email", "alice@test.example.com")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_credentials WHERE method='email' AND value='alice@test.example.com'").Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Got result but shouldn't", count)
|
|
}
|
|
|
|
err = adp.CredDel(types.ParseUserId("usr"+testData.Users[1].Id), "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_credentials WHERE userid=?", testData.Users[1].Id).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Got result but shouldn't", count)
|
|
}
|
|
}
|
|
|
|
func TestAuthDelScheme(t *testing.T) {
|
|
// tested during TestAuthUpdRecord
|
|
}
|
|
|
|
func TestAuthDelAllRecords(t *testing.T) {
|
|
delCount, err := adp.AuthDelAllRecords(types.ParseUserId("usr" + testData.Recs[0].UserId))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if delCount != 1 {
|
|
t.Error(mismatchErrorString("delCount", delCount, 1))
|
|
}
|
|
|
|
// With dummy user
|
|
delCount, _ = adp.AuthDelAllRecords(dummyUid1)
|
|
if delCount != 0 {
|
|
t.Error(mismatchErrorString("delCount", delCount, 0))
|
|
}
|
|
}
|
|
|
|
func TestSubsDelForUser(t *testing.T) {
|
|
// Tested during TestUserDelete (both hard and soft deletions)
|
|
}
|
|
|
|
func TestMessageDeleteList(t *testing.T) {
|
|
toDel := types.DelMessage{
|
|
ObjHeader: types.ObjHeader{
|
|
Id: testData.UGen.GetStr(),
|
|
CreatedAt: testData.Now,
|
|
UpdatedAt: testData.Now,
|
|
},
|
|
Topic: testData.Topics[1].Id,
|
|
DeletedFor: testData.Users[2].Id,
|
|
DelId: 1,
|
|
SeqIdRanges: []types.Range{{Low: 9}, {Low: 3, Hi: 7}},
|
|
}
|
|
err := adp.MessageDeleteList(toDel.Topic, &toDel)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check messages in dellog
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_dellog WHERE topic=? AND deletedfor=?",
|
|
toDel.Topic, decodeUid(toDel.DeletedFor)).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count == 0 {
|
|
t.Error("No im_dellog entries created")
|
|
}
|
|
|
|
// Hard delete test
|
|
toDel = types.DelMessage{
|
|
ObjHeader: types.ObjHeader{
|
|
Id: testData.UGen.GetStr(),
|
|
CreatedAt: testData.Now,
|
|
UpdatedAt: testData.Now,
|
|
},
|
|
Topic: testData.Topics[0].Id,
|
|
DelId: 3,
|
|
SeqIdRanges: []types.Range{{Low: 1, Hi: 3}},
|
|
}
|
|
err = adp.MessageDeleteList(toDel.Topic, &toDel)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check if messages content was cleared
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_messages WHERE topic=? AND content IS NOT NULL",
|
|
toDel.Topic).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count > 1 {
|
|
t.Errorf("Messages not properly deleted %d, %s", count, toDel.Topic)
|
|
}
|
|
|
|
err = adp.MessageDeleteList(testData.Topics[0].Id, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_messages WHERE topic=?", testData.Topics[0].Id).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Result should be empty:", count)
|
|
}
|
|
}
|
|
|
|
func TestTopicDelete(t *testing.T) {
|
|
err := adp.TopicDelete(testData.Topics[1].Id, false, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var state int
|
|
err = db.QueryRow("SELECT state FROM im_topics WHERE name=?", testData.Topics[1].Id).Scan(&state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state != int(types.StateDeleted) {
|
|
t.Error("Soft delete failed:", state)
|
|
}
|
|
|
|
err = adp.TopicDelete(testData.Topics[0].Id, false, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_topics WHERE name=?", testData.Topics[0].Id).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("Hard delete failed:", count)
|
|
}
|
|
}
|
|
|
|
func TestFileDeleteUnused(t *testing.T) {
|
|
locs, err := adp.FileDeleteUnused(time.Now().Add(1*time.Minute), 999)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(locs) != 2 {
|
|
t.Error(mismatchErrorString("Locations length", len(locs), 2))
|
|
}
|
|
}
|
|
|
|
func TestUserDelete(t *testing.T) {
|
|
err := adp.UserDelete(types.ParseUserId("usr"+testData.Users[0].Id), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var state int
|
|
err = db.QueryRow("SELECT state FROM im_users WHERE id=?",
|
|
decodeUid(testData.Users[0].Id)).Scan(&state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state != int(types.StateDeleted) {
|
|
t.Error("User soft delete failed", state)
|
|
}
|
|
|
|
err = adp.UserDelete(types.ParseUserId("usr"+testData.Users[1].Id), true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM im_users WHERE id=?", testData.Users[1].Id).Scan(&count)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Error("User hard delete failed")
|
|
}
|
|
}
|
|
|
|
// ================== Other tests =================================
|
|
|
|
func TestUserUnreadCount(t *testing.T) {
|
|
uids := []types.Uid{
|
|
types.ParseUserId("usr" + testData.Users[1].Id),
|
|
types.ParseUserId("usr" + testData.Users[2].Id),
|
|
}
|
|
expected := map[types.Uid]int{uids[0]: 0, uids[1]: 166}
|
|
counts, err := adp.UserUnreadCount(uids...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(counts) != 2 {
|
|
t.Error(mismatchErrorString("UnreadCount length", len(counts), 2))
|
|
}
|
|
|
|
for uid, unread := range counts {
|
|
if expected[uid] != unread {
|
|
t.Error(mismatchErrorString("UnreadCount", unread, expected[uid]))
|
|
}
|
|
}
|
|
|
|
// Test not found (even if the account is not found, the call must return one record).
|
|
counts, err = adp.UserUnreadCount(dummyUid1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(counts) != 1 {
|
|
t.Error(mismatchErrorString("UnreadCount length (dummy)", len(counts), 1))
|
|
}
|
|
if counts[dummyUid1] != 0 {
|
|
t.Error(mismatchErrorString("Non-zero UnreadCount (dummy)", counts[dummyUid1], 0))
|
|
}
|
|
}
|
|
|
|
func TestMessageGetDeleted(t *testing.T) {
|
|
qOpts := types.QueryOpt{
|
|
Since: 1,
|
|
Before: 10,
|
|
Limit: 999,
|
|
}
|
|
got, err := adp.MessageGetDeleted(testData.Topics[1].Id, types.ParseUserId("usr"+testData.Users[2].Id), &qOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Error(mismatchErrorString("result length", len(got), 1))
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
func mismatchErrorString(key string, got, want any) string {
|
|
return fmt.Sprintf("%s mismatch:\nGot = %+v\nWant = %+v", key, got, want)
|
|
}
|
|
|
|
func init() {
|
|
logs.Init(os.Stderr, "stdFlags")
|
|
adp = backend.GetTestAdapter()
|
|
conffile := flag.String("config", "./test.conf", "config of the database connection")
|
|
|
|
if file, err := os.Open(*conffile); err != nil {
|
|
log.Fatal("Failed to read config file:", err)
|
|
} else if err = json.NewDecoder(jcr.New(file)).Decode(&config); err != nil {
|
|
log.Fatal("Failed to parse config file:", err)
|
|
}
|
|
|
|
if adp == nil {
|
|
log.Fatal("Database adapter is missing")
|
|
}
|
|
if adp.IsOpen() {
|
|
log.Print("Connection is already opened")
|
|
}
|
|
testData = test_data.InitTestData()
|
|
if testData == nil {
|
|
log.Fatal("Failed to initialize test data")
|
|
}
|
|
store.SetTestUidGenerator(*testData.UGen)
|
|
|
|
err := adp.Open(config.Adapters[adp.GetName()])
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
db = adp.GetTestDB().(*sqlx.DB)
|
|
}
|