mirror of
https://github.com/tiennm99/telegram-exporter.git
synced 2026-10-11 03:13:49 +00:00
First slice of replacing the three-script pipeline with one process. The scripts coordinate tdl and rclone as separate programs, so everything expensive in them exists to work around the fact that neither can see the other's state. A single process does not need that machinery. This slice covers only the foundations: open the session tdl login already wrote, resolve an rclone destination, and report on both via a doctor command. Downloading, uploading and verification follow. The session store is shared with the tdl CLI rather than copied, so the two cannot run against one namespace at the same time; -n selects another. AppID and AppHash are read from the store rather than hardcoded, because a session is bound to the application that created it and tdl records which one it used. No middlewares are passed to tclient.New, which already prepends its own defaults; the DC pool gets them instead, since gotd applies a client's middlewares only to direct invocations and not to pooled connections. The rclone config is loaded up front because the lazy path calls os.Exit on a config it cannot read, which would bypass every defer and exit with the code this tool reserves for an incomplete run. Exit codes follow the shell pipeline: 0 ok, 1 incomplete, 2 usage, 3 remote failure, 130 SIGINT, 143 SIGTERM. Cancellation is checked explicitly after the Telegram client returns, because gotd reports an interrupted run as success and a driver would read that as a finished archive.
180 lines
4.9 KiB
Go
180 lines
4.9 KiB
Go
package tdlkv
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.etcd.io/bbolt"
|
|
|
|
"github.com/iyear/tdl/core/storage"
|
|
)
|
|
|
|
// newFixture writes a bolt file laid out the way tdl's driver lays one out:
|
|
// file named after the namespace, single bucket of the same name.
|
|
func newFixture(t *testing.T, dir, ns string, pairs map[string]string) {
|
|
t.Helper()
|
|
db, err := bbolt.Open(filepath.Join(dir, ns), 0o600, nil)
|
|
if err != nil {
|
|
t.Fatalf("create fixture: %v", err)
|
|
}
|
|
defer func() { _ = db.Close() }()
|
|
|
|
if err := db.Update(func(tx *bbolt.Tx) error {
|
|
b, err := tx.CreateBucketIfNotExists([]byte(ns))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range pairs {
|
|
if err := b.Put([]byte(k), []byte(v)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("seed fixture: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenReadsTdlLayout(t *testing.T) {
|
|
dir := t.TempDir()
|
|
newFixture(t, dir, "default", map[string]string{"app": "desktop"})
|
|
|
|
s, err := Open(dir, "default")
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer func() { _ = s.Close() }()
|
|
|
|
got, err := s.Get(context.Background(), "app")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if string(got) != "desktop" {
|
|
t.Errorf("Get(app) = %q, want %q", got, "desktop")
|
|
}
|
|
}
|
|
|
|
// A missing key must be distinguishable from an error, because core/storage
|
|
// consumers branch on ErrNotFound to fall back to defaults.
|
|
func TestGetMissingKeyReturnsErrNotFound(t *testing.T) {
|
|
dir := t.TempDir()
|
|
newFixture(t, dir, "default", nil)
|
|
|
|
s, err := Open(dir, "default")
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer func() { _ = s.Close() }()
|
|
|
|
if _, err := s.Get(context.Background(), "absent"); !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Get(absent) error = %v, want storage.ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestSetAndDeleteRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
newFixture(t, dir, "default", nil)
|
|
|
|
s, err := Open(dir, "default")
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer func() { _ = s.Close() }()
|
|
|
|
ctx := context.Background()
|
|
if err := s.Set(ctx, "session", []byte("blob")); err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
got, err := s.Get(ctx, "session")
|
|
if err != nil || string(got) != "blob" {
|
|
t.Fatalf("Get after Set = %q, %v", got, err)
|
|
}
|
|
if err := s.Delete(ctx, "session"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := s.Get(ctx, "session"); !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Get after Delete error = %v, want storage.ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
// The three failure modes must stay distinguishable in the message, because
|
|
// each one tells the operator to do something different.
|
|
func TestOpenFailureMessages(t *testing.T) {
|
|
t.Run("missing namespace file", func(t *testing.T) {
|
|
_, err := Open(t.TempDir(), "default")
|
|
if err == nil {
|
|
t.Fatal("expected an error for a missing session file")
|
|
}
|
|
if !strings.Contains(err.Error(), "tdl login") {
|
|
t.Errorf("error should point at `tdl login`, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("non-default namespace names the flag", func(t *testing.T) {
|
|
_, err := Open(t.TempDir(), "second")
|
|
if err == nil {
|
|
t.Fatal("expected an error for a missing session file")
|
|
}
|
|
if !strings.Contains(err.Error(), "-n second") {
|
|
t.Errorf("error should name the namespace flag, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("file present but bucket missing", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
db, err := bbolt.Open(filepath.Join(dir, "default"), 0o600, nil)
|
|
if err != nil {
|
|
t.Fatalf("create empty db: %v", err)
|
|
}
|
|
_ = db.Close()
|
|
|
|
if _, err := Open(dir, "default"); err == nil {
|
|
t.Fatal("expected an error for a bucketless store")
|
|
} else if !strings.Contains(err.Error(), "incomplete") {
|
|
t.Errorf("error should call the session incomplete, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty namespace rejected", func(t *testing.T) {
|
|
if _, err := Open(t.TempDir(), ""); err == nil {
|
|
t.Fatal("expected an error for an empty namespace")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Open must not create a session that does not exist: silently handing back an
|
|
// empty store would surface much later as a confusing auth failure.
|
|
func TestOpenDoesNotCreateMissingStore(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, err := Open(dir, "default"); err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "default")); !os.IsNotExist(err) {
|
|
t.Errorf("Open created %s; it must never create a session file", filepath.Join(dir, "default"))
|
|
}
|
|
}
|
|
|
|
// A second opener must be told the store is locked rather than hanging.
|
|
func TestOpenReportsLockContention(t *testing.T) {
|
|
dir := t.TempDir()
|
|
newFixture(t, dir, "default", nil)
|
|
|
|
first, err := Open(dir, "default")
|
|
if err != nil {
|
|
t.Fatalf("first Open: %v", err)
|
|
}
|
|
defer func() { _ = first.Close() }()
|
|
|
|
_, err = Open(dir, "default")
|
|
if err == nil {
|
|
t.Fatal("expected the second Open to fail while the first holds the lock")
|
|
}
|
|
if !strings.Contains(err.Error(), "locked by another process") {
|
|
t.Errorf("error should name lock contention, got: %v", err)
|
|
}
|
|
}
|