package postgres import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "strings" "testing" "telesrv/internal/domain" "telesrv/internal/store" ) func TestBusinessStoresRoundTrip(t *testing.T) { pool := testPool(t) ctx := context.Background() suffix := randomSuffix(t) var authID [8]byte var authBody [256]byte if _, err := rand.Read(authID[:]); err != nil { t.Fatal(err) } if _, err := rand.Read(authBody[:]); err != nil { t.Fatal(err) } if err := NewAuthKeyStore(pool).Save(ctx, store.AuthKeyData{ ID: authID, Value: authBody, ServerSalt: 42, }); err != nil { t.Fatalf("save auth key: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM auth_keys WHERE auth_key_id = $1", authKeyIDToInt64(authID)) }) users := NewUserStore(pool) owner, err := users.Create(ctx, domain.User{ AccessHash: 1, Phone: "+1555" + suffix + "01", FirstName: "Owner", }) if err != nil { t.Fatalf("create owner: %v", err) } if owner.ID < domain.UserIDSequenceBase { t.Fatalf("owner id = %d, want >= base %d", owner.ID, domain.UserIDSequenceBase) } friend, err := users.Create(ctx, domain.User{ AccessHash: 2, Phone: "+1555" + suffix + "02", FirstName: "Friend", }) if err != nil { t.Fatalf("create friend: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, friend.ID}) }) username := "owner_" + suffix owner, err = users.UpdateUsername(ctx, owner.ID, username) if err != nil { t.Fatalf("update owner username: %v", err) } if owner.Username != username { t.Fatalf("owner username = %q, want %q", owner.Username, username) } byUsername, found, err := users.ByUsername(ctx, strings.ToUpper(username)) if err != nil || !found || byUsername.ID != owner.ID { t.Fatalf("by username = user %+v found %v err %v, want owner", byUsername, found, err) } if _, err := users.UpdateUsername(ctx, friend.ID, strings.ToUpper(username)); !errors.Is(err, domain.ErrUsernameOccupied) { t.Fatalf("duplicate username err = %v, want username occupied", err) } if _, err := pool.Exec(ctx, ` INSERT INTO contacts (user_id, contact_user_id, mutual) VALUES ($1, $2, true) `, owner.ID, friend.ID); err != nil { t.Fatalf("insert contact: %v", err) } contacts, err := NewContactStore(pool).ListByUser(ctx, owner.ID) if err != nil { t.Fatalf("list contacts: %v", err) } if len(contacts.Contacts) != 1 || contacts.Contacts[0].User.ID != friend.ID || !contacts.Contacts[0].Mutual { t.Fatalf("contacts = %+v, want friend mutual contact", contacts) } if contacts.Hash == 0 { t.Fatal("contacts hash is zero for non-empty contact list") } search, err := users.Search(ctx, owner.ID, "Friend", "", 10) if err != nil { t.Fatalf("search users: %v", err) } if len(search.MyResults) != 1 || search.MyResults[0].ID != friend.ID || !search.MyResults[0].Contact { t.Fatalf("search contacts = %+v, want friend in my results", search) } if _, err := pool.Exec(ctx, ` INSERT INTO dialogs (user_id, peer_type, peer_id, top_message_id, unread_count, pinned) VALUES ($1, 'user', $2, 10, 2, true) `, owner.ID, friend.ID); err != nil { t.Fatalf("insert dialog: %v", err) } dialogs, err := NewDialogStore(pool).ListByUser(ctx, owner.ID, domain.DialogFilter{PinnedOnly: true, Limit: 10}) if err != nil { t.Fatalf("list dialogs: %v", err) } if len(dialogs.Dialogs) != 1 || dialogs.Dialogs[0].Peer.ID != friend.ID || !dialogs.Dialogs[0].Pinned { t.Fatalf("dialogs = %+v, want pinned friend dialog", dialogs) } emptyDialogsPage, err := NewDialogStore(pool).ListByUser(ctx, owner.ID, domain.DialogFilter{ PinnedOnly: true, OffsetID: dialogs.Dialogs[0].TopMessage, HasOffsetPeer: true, OffsetPeer: dialogs.Dialogs[0].Peer, Limit: 10, }) if err != nil { t.Fatalf("list empty dialogs page: %v", err) } if len(emptyDialogsPage.Dialogs) != 0 || emptyDialogsPage.Count != dialogs.Count || emptyDialogsPage.Hash != dialogs.Hash { t.Fatalf("empty dialogs page = %+v, want empty rows with full count/hash from %+v", emptyDialogsPage, dialogs) } peerDialogs, err := NewDialogStore(pool).ListByPeers(ctx, owner.ID, []domain.Peer{ {Type: domain.PeerTypeUser, ID: friend.ID}, {Type: domain.PeerTypeUser, ID: owner.ID}, }) if err != nil { t.Fatalf("list peer dialogs: %v", err) } if len(peerDialogs.Dialogs) != 2 || peerDialogs.Dialogs[0].Peer.ID != friend.ID || peerDialogs.Dialogs[0].TopMessage != 10 { t.Fatalf("peer dialogs = %+v, want friend dialog plus owner placeholder", peerDialogs) } if peerDialogs.Dialogs[1].Peer.ID != owner.ID || peerDialogs.Dialogs[1].TopMessage != 0 { t.Fatalf("placeholder dialog = %+v, want owner empty dialog", peerDialogs.Dialogs[1]) } if len(peerDialogs.Users) != 2 { t.Fatalf("peer dialog users = %+v, want both requested users", peerDialogs.Users) } wantState := domain.UpdateState{Pts: 11, Qts: 12, Date: 13, Seq: 14} states := NewUpdateStateStore(pool) if err := states.Save(ctx, authID, owner.ID, wantState); err != nil { t.Fatalf("save update state: %v", err) } gotState, found, err := states.Get(ctx, authID, owner.ID) if err != nil { t.Fatalf("get update state: %v", err) } if !found || gotState != wantState { t.Fatalf("update state = %+v found=%v, want %+v found=true", gotState, found, wantState) } if err := states.Delete(ctx, authID, owner.ID); err != nil { t.Fatalf("delete update state: %v", err) } if _, found, err := states.Get(ctx, authID, owner.ID); err != nil || found { t.Fatalf("state after delete found=%v err=%v, want not found", found, err) } if err := NewTempAuthKeyBindingStore(pool).Save(ctx, domain.TempAuthKeyBinding{ TempAuthKeyID: authID, PermAuthKeyID: 12345, Nonce: 67890, TempSessionID: 24680, ExpiresAt: 111, EncryptedMessage: []byte("binding"), }); err != nil { t.Fatalf("save temp auth key binding: %v", err) } var bindingCount int var tempSessionID int64 if err := pool.QueryRow(ctx, "SELECT count(*), coalesce(max(temp_session_id), 0) FROM temp_auth_key_bindings WHERE temp_auth_key_id = $1", authKeyIDToInt64(authID)).Scan(&bindingCount, &tempSessionID); err != nil { t.Fatalf("count temp auth key binding: %v", err) } if bindingCount != 1 || tempSessionID != 24680 { t.Fatalf("temp auth key binding count/session = %d/%d, want 1/24680", bindingCount, tempSessionID) } passwords := NewPasswordStore(pool) wantPassword := domain.PasswordSettings{ Hint: "dev", SecureRandom: []byte("secure-random"), } if err := passwords.Save(ctx, owner.ID, wantPassword); err != nil { t.Fatalf("save password settings: %v", err) } gotPassword, found, err := passwords.GetByUser(ctx, owner.ID) if err != nil { t.Fatalf("get password settings: %v", err) } if !found || gotPassword.Hint != wantPassword.Hint || string(gotPassword.SecureRandom) != string(wantPassword.SecureRandom) { t.Fatalf("password settings = %+v found=%v, want %+v found=true", gotPassword, found, wantPassword) } help := NewHelpStore(pool) client := "tdesktop-test-" + suffix t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM app_configs WHERE client = $1", client) }) if err := help.UpsertAppConfig(ctx, domain.AppConfig{Client: client, Hash: 9, JSON: []byte(`{"test":true}`)}); err != nil { t.Fatalf("upsert app config: %v", err) } cfg, found, err := help.GetAppConfig(ctx, client) if err != nil { t.Fatalf("get app config: %v", err) } if !found || cfg.Hash != 9 || string(cfg.JSON) != `{"test": true}` { t.Fatalf("app config = %+v found=%v, want hash=9 json", cfg, found) } countryISO := "T" + suffix[:1] t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM countries WHERE iso2 = $1", countryISO) }) if err := help.UpsertCountries(ctx, []domain.Country{{ ISO2: countryISO, DefaultName: "Testland", CountryCodes: []domain.CountryCode{ {CountryCode: "999", Prefixes: []string{"999"}, Patterns: []string{"XXX"}}, }, }}); err != nil { t.Fatalf("upsert countries: %v", err) } countryList, err := help.ListCountries(ctx, "en") if err != nil { t.Fatalf("list countries: %v", err) } var foundCountry bool for _, country := range countryList.Countries { if country.ISO2 == countryISO && len(country.CountryCodes) == 1 && country.CountryCodes[0].CountryCode == "999" { foundCountry = true } } if !foundCountry { t.Fatalf("country %s not found in %+v", countryISO, countryList) } langCode := "test-" + suffix lang := NewLangPackStore(pool) t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM lang_packs WHERE lang_pack = $1 AND lang_code = $2", "tdesktop", langCode) }) if err := lang.UpsertPack(ctx, domain.LangPack{ LangPack: "tdesktop", LangCode: langCode, Version: 7, Strings: []domain.LangPackString{ {Key: "lng_test", Value: "Test"}, {Key: "lng_items", Pluralized: true, OneValue: "{count} item", OtherValue: "{count} items"}, }, }); err != nil { t.Fatalf("upsert lang pack: %v", err) } pack, err := lang.GetPack(ctx, "tdesktop", langCode, 0) if err != nil { t.Fatalf("get lang pack: %v", err) } if pack.Version != 7 || len(pack.Strings) != 2 { t.Fatalf("lang pack = %+v, want version 7 with 2 strings", pack) } notModified, err := lang.GetPack(ctx, "tdesktop", langCode, 7) if err != nil { t.Fatalf("get lang pack not modified: %v", err) } if notModified.Version != 7 || len(notModified.Strings) != 0 { t.Fatalf("not modified pack = %+v, want version 7 with no strings", notModified) } selected, err := lang.GetStrings(ctx, "tdesktop", langCode, []string{"lng_test"}) if err != nil { t.Fatalf("get lang pack strings: %v", err) } if len(selected.Strings) != 1 || selected.Strings[0].Value != "Test" { t.Fatalf("selected strings = %+v, want lng_test=Test", selected.Strings) } } func randomSuffix(t *testing.T) string { t.Helper() var b [4]byte if _, err := rand.Read(b[:]); err != nil { t.Fatal(err) } return fmt.Sprintf("%s", hex.EncodeToString(b[:])) }