feat: complete registration invitation and auth redesign

This commit is contained in:
richjohnrambo
2026-08-19 00:30:11 +08:00
parent e063e7879e
commit 219ecadf4c
47 changed files with 9014 additions and 224 deletions
+93 -16
View File
@@ -126,6 +126,20 @@ func TestUserIdentitySchema(t *testing.T) {
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)
@@ -275,12 +289,12 @@ func TestTenantAuthIsolation(t *testing.T) {
userBID := userAID + 1
for index, userID := range []int64{userAID, userBID} {
tenantDB := quoteIdent(dbNames[index])
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,createdat,updatedat,state,access,phone,public,trusted) "+
"VALUES(?,?,?,?,?,?,?,?)", userID, now, now, types.StateOK, `{}`, "13800000000", nil, nil); err != nil {
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,username,createdat,updatedat,state,access,phone,public,trusted) "+
"VALUES(?,?,?,?,?,?,?,?,?)", userID, "alice", now, now, types.StateOK, `{}`, "13800000000", nil, nil); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(uname,userid,scheme,authlvl,secret) VALUES(?,?,?,?,?)",
"basic:alice", userID, "basic", auth.LevelAuth, []byte("hash")); err != nil {
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) "+
@@ -289,11 +303,11 @@ func TestTenantAuthIsolation(t *testing.T) {
}
}
uidA, _, _, _, err := tenantAdp.TenantAuthGetUniqueRecord(tenantA, "basic:alice")
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.TenantAuthGetUniqueRecord(tenantB, "basic:alice")
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)
}
@@ -306,7 +320,7 @@ func TestTenantAuthIsolation(t *testing.T) {
t.Fatalf("tenant B credential lookup: uid=%v err=%v", credB, err)
}
if err = tenantAdp.TenantAuthUpdRecord(tenantA, uidB, "basic", "basic:changed",
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)
}
@@ -382,7 +396,7 @@ func TestCredUpsert(t *testing.T) {
func TestAuthAddRecord(t *testing.T) {
for _, rec := range testData.Recs {
err := adp.AuthAddRecord(types.ParseUserId("usr"+rec.UserId), rec.Scheme, rec.Unique,
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)
@@ -390,12 +404,74 @@ func TestAuthAddRecord(t *testing.T) {
}
//Test duplicate
err := adp.AuthAddRecord(types.ParseUserId("usr"+testData.Users[0].Id), testData.Recs[0].Scheme,
testData.Recs[0].Unique, testData.Recs[0].AuthLvl, testData.Recs[0].Secret, testData.Recs[0].Expires)
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 TestTopicCreate(t *testing.T) {
err := adp.TopicCreate(testData.Topics[0])
if err != nil {
@@ -610,7 +686,7 @@ func TestCredGetAll(t *testing.T) {
}
func TestAuthGetUniqueRecord(t *testing.T) {
uid, authLvl, secret, expires, err := adp.AuthGetUniqueRecord("basic:alice")
uid, authLvl, secret, expires, err := adp.AuthGetUniqueRecord("alice")
if err != nil {
t.Fatal(err)
}
@@ -636,13 +712,14 @@ func TestAuthGetRecord(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if recId != testData.Recs[0].Unique ||
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", testData.Recs[0].Unique, testData.Recs[0].AuthLvl, testData.Recs[0].Secret, testData.Recs[0].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))
}
@@ -957,13 +1034,13 @@ func TestCredConfirm(t *testing.T) {
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, rec.Unique,
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 uname=?", rec.Unique).Scan(&got)
err = db.QueryRow("SELECT secret FROM im_auth WHERE userid=?", decodeUid(rec.UserId)).Scan(&got)
if err != nil {
t.Fatal(err)
}
@@ -972,7 +1049,7 @@ func TestAuthUpdRecord(t *testing.T) {
}
// Test with auth ID (unique) change
newId := "basic:bob12345"
newId := "bob12345"
err = adp.AuthUpdRecord(types.ParseUserId("usr"+rec.UserId), rec.Scheme, newId,
rec.AuthLvl, newSecret, rec.Expires)
if err != nil {
@@ -980,7 +1057,7 @@ func TestAuthUpdRecord(t *testing.T) {
}
// Test if old ID deleted
var count int
err = db.QueryRow("SELECT COUNT(*) FROM im_auth WHERE uname=?", rec.Unique).Scan(&count)
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)
}
+5 -3
View File
@@ -1,10 +1,12 @@
{
"reset_db_data": true,
"adapters": {
"mysql": {
"dsn": "root@tcp(localhost:3306)/tinode_test?parseTime=true&collation=utf8mb4_unicode_ci",
"mysql": {
"dsn": "root:root@tcp(127.0.0.1:3306)/tinode_test?parseTime=true&collation=utf8mb4_unicode_ci",
// Name of the main database.
"database": "tinode_test"
"database": "tinode_test",
"tenant_registry_db": "tinode_test",
"tenant_db_routing": false
}
}
}