package apply import ( "bytes" "context" "fmt" "path/filepath" "strings" "testing" "time" "github.com/tegwick/fluid-telegram/internal/plan" "github.com/tegwick/fluid-telegram/internal/secrets" "github.com/tegwick/fluid-telegram/internal/spec" "github.com/tegwick/fluid-telegram/internal/state" tgc "github.com/tegwick/fluid-telegram/internal/tg" ) type memStore struct { data map[string]map[string]string } func (s *memStore) CreateIfAbsent(_ context.Context, key string, fields map[string]string) (bool, error) { if s.data == nil { s.data = map[string]map[string]string{} } if _, ok := s.data[key]; ok { return false, nil } s.data[key] = fields return true, nil } func (s *memStore) Put(_ context.Context, key string, fields map[string]string) error { if s.data == nil { s.data = map[string]map[string]string{} } s.data[key] = fields return nil } func (s *memStore) Ref(key string) string { return "bao:test/" + key } type fakeConv struct { taken map[string]bool profiles int } func (c *fakeConv) RegisterBot(_ context.Context, _, username string) (string, string, error) { if c.taken[username] { return "", "", fmt.Errorf("%w: @%s", tgc.ErrUsernameTaken, username) } return username, "123:token-for-" + username, nil } func (c *fakeConv) SetProfile(context.Context, string, string, string, string) error { c.profiles++ return nil } func (c *fakeConv) SetAvatar(context.Context, string, string) error { return nil } type fakeSurface struct { conv *fakeConv nextChat int64 created []string claimed []string promoted []string titles map[int64]string abouts map[int64]string botID int64 } func (f *fakeSurface) OpenBotFather(context.Context) (BotConversation, error) { if f.conv == nil { f.conv = &fakeConv{} } return f.conv, nil } func (f *fakeSurface) ResolveBot(_ context.Context, _ string) (int64, error) { if f.botID == 0 { f.botID = 42 } return f.botID, nil } func (f *fakeSurface) CreateChannel(_ context.Context, title, about string) (int64, error) { f.nextChat-- id := f.nextChat f.created = append(f.created, title) if f.titles == nil { f.titles = map[int64]string{} f.abouts = map[int64]string{} } f.titles[id] = title f.abouts[id] = about return id, nil } func (f *fakeSurface) ClaimUsername(_ context.Context, _ int64, username string) (bool, error) { f.claimed = append(f.claimed, username) return true, nil } func (f *fakeSurface) PromoteBot(_ context.Context, _ int64, botUsername string) error { f.promoted = append(f.promoted, botUsername) return nil } func (f *fakeSurface) EditChannelTitle(_ context.Context, chatID int64, title string) error { if f.titles == nil { f.titles = map[int64]string{} } f.titles[chatID] = title return nil } func (f *fakeSurface) EditChannelAbout(_ context.Context, chatID int64, about string) error { if f.abouts == nil { f.abouts = map[int64]string{} } f.abouts[chatID] = about return nil } func presence() *spec.Presence { return &spec.Presence{ SchemaVersion: "0.1", Campaign: "hall-of-helix", Interface: "i", Bot: spec.Bot{Name: "HelixForge", UsernamePreference: []string{"HelixForgeBot"}, About: "about", Description: "desc"}, Channels: map[string]spec.Channel{ spec.Test: {Title: "test-title", Description: "test-about", Visibility: spec.Private}, spec.Live: {Title: "live-title", Description: "live-about", Visibility: spec.Public, UsernamePreference: []string{"hallofhelix"}}, }, } } func opts(t *testing.T, rs *state.Resolved, surf *fakeSurface, store *memStore) Options { t.Helper() if rs == nil { rs = &state.Resolved{} } if surf == nil { surf = &fakeSurface{} } if store == nil { store = &memStore{} } return Options{ Spec: presence(), SpecDigest: "sha256:x", State: rs, StatePath: filepath.Join(t.TempDir(), "resolved.yaml"), Store: store, Surface: surf, Out: &bytes.Buffer{}, } } func TestApplyRefusesBlockedPlan(t *testing.T) { p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Block, Target: "bot", Detail: "gone"}}} err := Run(context.Background(), p, opts(t, nil, nil, nil)) if err == nil { t.Fatal("applied a blocked plan") } } func TestFirstRunCreatesBotAndTestChannelOnly(t *testing.T) { surf := &fakeSurface{} store := &memStore{} o := opts(t, nil, surf, store) p := &plan.Plan{Actions: []plan.Action{ {Kind: plan.Attempt, Target: "bot"}, {Kind: plan.Create, Target: "channel.test"}, {Kind: plan.Defer, Target: "channel.public"}, }} if err := Run(context.Background(), p, o); err != nil { t.Fatal(err) } if store.data[secrets.KeyBotToken]["token"] == "" { t.Fatal("bot token was not stored") } if _, ok := store.data[secrets.KeyRedactionSalt]; !ok { t.Fatal("redaction salt was not created") } if o.State.Bot.Username != "HelixForgeBot" || o.State.Bot.ID == 0 { t.Fatalf("bot state = %+v", o.State.Bot) } if _, ok := o.State.Channels[spec.Test]; !ok { t.Fatal("test channel was not recorded") } if _, ok := o.State.Channels[spec.Live]; ok { t.Fatal("public channel was created before the test channel was verified") } if len(surf.promoted) != 1 { t.Fatalf("promotions = %v", surf.promoted) } } func TestWalksUsernameCandidates(t *testing.T) { surf := &fakeSurface{conv: &fakeConv{taken: map[string]bool{"HelixForgeBot": true}}} sp := presence() sp.Bot.UsernamePreference = []string{"HelixForgeBot", "HelixForgePublishBot"} o := opts(t, nil, surf, nil) o.Spec = sp p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Attempt, Target: "bot"}}} if err := Run(context.Background(), p, o); err != nil { t.Fatal(err) } if o.State.Bot.Username != "HelixForgePublishBot" { t.Fatalf("username = %s", o.State.Bot.Username) } } func TestExistingBotReassertsProfile(t *testing.T) { surf := &fakeSurface{} now := time.Now() rs := &state.Resolved{ Campaign: "hall-of-helix", SpecDigest: "sha256:x", Bot: state.Bot{Username: "HelixForgeBot", ID: 42}, Channels: map[string]state.Channel{ spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}, TestPublicationAt: &now}, spec.Live: {ChatID: -200, Username: "hallofhelix", AdminRights: []string{spec.PostMessages}}, }, } o := opts(t, rs, surf, &memStore{data: map[string]map[string]string{ secrets.KeyRedactionSalt: {"salt": "already"}, }}) p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Reassert, Target: "bot.profile"}}} if err := Run(context.Background(), p, o); err != nil { t.Fatal(err) } if surf.conv == nil || surf.conv.profiles == 0 { t.Fatal("existing bot did not reassert its profile") } if len(surf.created) != 0 { t.Fatalf("recreated channels: %v", surf.created) } } func TestSpecChangeUpdatesChannelCopy(t *testing.T) { surf := &fakeSurface{} now := time.Now() rs := &state.Resolved{ Campaign: "hall-of-helix", SpecDigest: "sha256:old", Bot: state.Bot{Username: "HelixForgeBot", ID: 42}, Channels: map[string]state.Channel{ spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}, TestPublicationAt: &now}, spec.Live: {ChatID: -200, Username: "hallofhelix", AdminRights: []string{spec.PostMessages}}, }, } o := opts(t, rs, surf, &memStore{data: map[string]map[string]string{ secrets.KeyRedactionSalt: {"salt": "already"}, }}) p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Update, Target: "spec"}}} if err := Run(context.Background(), p, o); err != nil { t.Fatal(err) } if surf.titles[-100] != "test-title" || surf.titles[-200] != "live-title" { t.Fatalf("titles = %v", surf.titles) } } func TestSaltIsCreatedOnce(t *testing.T) { store := &memStore{} p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Attempt, Target: "bot"}}} if err := Run(context.Background(), p, opts(t, nil, nil, store)); err != nil { t.Fatal(err) } first := store.data[secrets.KeyRedactionSalt]["salt"] rs := &state.Resolved{ Campaign: "hall-of-helix", Bot: state.Bot{Username: "HelixForgeBot", ID: 42}, Channels: map[string]state.Channel{ spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}}, }, } if err := Run(context.Background(), p, opts(t, rs, nil, store)); err != nil { t.Fatal(err) } if store.data[secrets.KeyRedactionSalt]["salt"] != first { t.Fatal("redaction salt was rotated") } } func TestPublicReleasedAfterTestPublication(t *testing.T) { now := time.Now() rs := &state.Resolved{ Campaign: "hall-of-helix", Bot: state.Bot{Username: "HelixForgeBot", ID: 42}, Channels: map[string]state.Channel{ spec.Test: {ChatID: -100, AdminRights: []string{spec.PostMessages}, TestPublicationAt: &now}, }, } surf := &fakeSurface{} o := opts(t, rs, surf, &memStore{data: map[string]map[string]string{ secrets.KeyRedactionSalt: {"salt": "already"}, }}) p := &plan.Plan{Actions: []plan.Action{{Kind: plan.Create, Target: "channel.public"}}} if err := Run(context.Background(), p, o); err != nil { t.Fatal(err) } pub, ok := o.State.Channels[spec.Live] if !ok { t.Fatal("public channel was not created") } if pub.Username != "hallofhelix" { t.Fatalf("username = %s", pub.Username) } if !strings.Contains(strings.Join(surf.promoted, ","), "HelixForgeBot") { t.Fatalf("bot was not promoted on public: %v", surf.promoted) } }