Initial open source release
This commit is contained in:
commit
74992e893f
377 changed files with 118084 additions and 0 deletions
288
internal/store/postgres/business_integration_test.go
Normal file
288
internal/store/postgres/business_integration_test.go
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
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[:]))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue