• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

umputun / tg-spam / 15672803049

16 Jun 2025 05:42AM UTC coverage: 79.352% (-2.1%) from 81.499%
15672803049

Pull #294

github

umputun
Add CLI override functionality for auth credentials in database mode

- Created applyCLIOverrides function to handle selective CLI parameter overrides
- Currently handles --server.auth and --server.auth-hash overrides
- Only applies overrides when values differ from defaults
- Auth hash takes precedence over password when both are provided
- Added comprehensive unit tests covering all override scenarios
- Function is extensible for future CLI override needs (documented in comments)

This fixes the issue where users couldn't change auth credentials when using
database configuration mode (--confdb), as the save-config command would
overwrite all settings rather than just the auth credentials.
Pull Request #294: Implement database configuration support

891 of 1298 new or added lines in 9 files covered. (68.64%)

174 existing lines in 4 files now uncovered.

5734 of 7226 relevant lines covered (79.35%)

57.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.5
/app/storage/config.go
1
package storage
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "time"
8

9
        "github.com/jmoiron/sqlx"
10

11
        "github.com/umputun/tg-spam/app/storage/engine"
12
)
13

14
// Config provides access to configuration stored in database
15
type Config[T any] struct {
16
        *engine.SQL
17
        engine.RWLocker
18
}
19

20
// ConfigRecord represents a configuration entry
21
type ConfigRecord struct {
22
        GID       string    `db:"gid"`
23
        Data      string    `db:"data"`
24
        CreatedAt time.Time `db:"created_at"`
25
        UpdatedAt time.Time `db:"updated_at"`
26
}
27

28
// all config queries
29
const (
30
        CmdCreateConfigTable engine.DBCmd = iota + 200
31
        CmdCreateConfigIndexes
32
        CmdSetConfig
33
)
34

35
// queries holds all config queries
36
var configQueries = engine.NewQueryMap().
37
        Add(CmdCreateConfigTable, engine.Query{
38
                Sqlite: `CREATE TABLE IF NOT EXISTS config (
39
                        id INTEGER PRIMARY KEY,
40
                        gid TEXT NOT NULL,
41
                        data TEXT NOT NULL,
42
                        created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
                        updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
44
                        UNIQUE(gid)
45
                )`,
46
                Postgres: `CREATE TABLE IF NOT EXISTS config (
47
                        id SERIAL PRIMARY KEY,
48
                        gid TEXT NOT NULL,
49
                        data TEXT NOT NULL,
50
                        created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
51
                        updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
52
                        UNIQUE(gid)
53
                )`,
54
        }).
55
        AddSame(CmdCreateConfigIndexes, `
56
                CREATE INDEX IF NOT EXISTS idx_config_gid ON config(gid)
57
        `).
58
        Add(CmdSetConfig, engine.Query{
59
                Sqlite: `INSERT INTO config (gid, data, updated_at) 
60
                        VALUES (?, ?, ?) 
61
                        ON CONFLICT (gid) DO UPDATE 
62
                        SET data = excluded.data, updated_at = excluded.updated_at`,
63
                Postgres: `INSERT INTO config (gid, data, updated_at) 
64
                        VALUES ($1, $2, $3) 
65
                        ON CONFLICT (gid) DO UPDATE 
66
                        SET data = EXCLUDED.data, updated_at = EXCLUDED.updated_at`,
67
        })
68

69
// NewConfig creates new config instance
70
func NewConfig[T any](ctx context.Context, db *engine.SQL) (*Config[T], error) {
31✔
71
        if db == nil {
33✔
72
                return nil, fmt.Errorf("no db provided")
2✔
73
        }
2✔
74

75
        res := &Config[T]{SQL: db, RWLocker: db.MakeLock()}
29✔
76
        cfg := engine.TableConfig{
29✔
77
                Name:          "config",
29✔
78
                CreateTable:   CmdCreateConfigTable,
29✔
79
                CreateIndexes: CmdCreateConfigIndexes,
29✔
80
                QueriesMap:    configQueries,
29✔
81
                MigrateFunc:   func(_ context.Context, _ *sqlx.Tx, _ string) error { return nil }, // no-op migrate function
56✔
82
        }
83
        if err := engine.InitTable(ctx, db, cfg); err != nil {
31✔
84
                return nil, fmt.Errorf("failed to init config table: %w", err)
2✔
85
        }
2✔
86
        return res, nil
27✔
87
}
88

89
// Get retrieves the configuration for the current group
90
func (c *Config[T]) Get(ctx context.Context) (string, error) {
46✔
91
        c.RLock()
46✔
92
        defer c.RUnlock()
46✔
93

46✔
94
        var record ConfigRecord
46✔
95
        query := c.Adopt("SELECT data FROM config WHERE gid = ?")
46✔
96
        err := c.GetContext(ctx, &record, query, c.GID())
46✔
97
        if err != nil {
54✔
98
                if err.Error() == "sql: no rows in result set" {
14✔
99
                        return "", fmt.Errorf("no configuration found")
6✔
100
                }
6✔
101
                return "", fmt.Errorf("failed to get config: %w", err)
2✔
102
        }
103
        return record.Data, nil
38✔
104
}
105

106
// GetObject retrieves the configuration and deserializes it into the provided struct
107
func (c *Config[T]) GetObject(ctx context.Context, obj *T) error {
42✔
108
        data, err := c.Get(ctx)
42✔
109
        if err != nil {
48✔
110
                return err
6✔
111
        }
6✔
112
        if err := json.Unmarshal([]byte(data), obj); err != nil {
36✔
NEW
113
                return fmt.Errorf("failed to unmarshal config: %w", err)
×
NEW
114
        }
×
115
        return nil
36✔
116
}
117

118
// Set updates or creates the configuration
119
func (c *Config[T]) Set(ctx context.Context, data string) error {
54✔
120
        if data == "" {
56✔
121
                return fmt.Errorf("empty data not allowed")
2✔
122
        }
2✔
123

124
        if data == "null" {
54✔
125
                return fmt.Errorf("null value not allowed")
2✔
126
        }
2✔
127

128
        // validate JSON structure
129
        var jsonCheck interface{}
50✔
130
        if err := json.Unmarshal([]byte(data), &jsonCheck); err != nil {
52✔
131
                return fmt.Errorf("invalid JSON: %w", err)
2✔
132
        }
2✔
133

134
        // validate against the generic type
135
        var typeCheck T
48✔
136
        if err := json.Unmarshal([]byte(data), &typeCheck); err != nil {
52✔
137
                return fmt.Errorf("invalid type: %w", err)
4✔
138
        }
4✔
139

140
        c.Lock()
44✔
141
        defer c.Unlock()
44✔
142

44✔
143
        query, err := configQueries.Pick(c.Type(), CmdSetConfig)
44✔
144
        if err != nil {
44✔
NEW
145
                return fmt.Errorf("failed to get set query: %w", err)
×
NEW
146
        }
×
147

148
        // for PostgreSQL, we need to use transactions
149
        if c.Type() == engine.Postgres {
66✔
150
                return c.setConfigPostgres(ctx, query, data)
22✔
151
        }
22✔
152

153
        // for SQLite, the RWLock is sufficient
154
        _, err = c.ExecContext(ctx, query, c.GID(), data, time.Now())
22✔
155
        if err != nil {
23✔
156
                return fmt.Errorf("failed to set config: %w", err)
1✔
157
        }
1✔
158
        return nil
21✔
159
}
160

161
// SetObject serializes an object to JSON and stores it
162
func (c *Config[T]) SetObject(ctx context.Context, obj *T) error {
44✔
163
        data, err := json.Marshal(obj)
44✔
164
        if err != nil {
44✔
NEW
165
                return fmt.Errorf("failed to marshal config: %w", err)
×
NEW
166
        }
×
167
        return c.Set(ctx, string(data))
44✔
168
}
169

170
// Delete removes the configuration
171
func (c *Config[T]) Delete(ctx context.Context) error {
8✔
172
        c.Lock()
8✔
173
        defer c.Unlock()
8✔
174

8✔
175
        query := c.Adopt("DELETE FROM config WHERE gid = ?")
8✔
176
        _, err := c.ExecContext(ctx, query, c.GID())
8✔
177
        if err != nil {
10✔
178
                return fmt.Errorf("failed to delete config: %w", err)
2✔
179
        }
2✔
180
        return nil
6✔
181
}
182

183
// setConfigPostgres handles PostgreSQL-specific configuration storage with transaction
184
func (c *Config[T]) setConfigPostgres(ctx context.Context, query, data string) error {
22✔
185
        // begin transaction
22✔
186
        tx, err := c.BeginTxx(ctx, nil)
22✔
187
        if err != nil {
23✔
188
                return fmt.Errorf("failed to begin transaction: %w", err)
1✔
189
        }
1✔
190

191
        // set up rollback on error
192
        defer func() {
42✔
193
                if err != nil {
21✔
NEW
194
                        _ = tx.Rollback()
×
NEW
195
                }
×
196
        }()
197

198
        // lock the row for update to ensure serialization
199
        _, err = tx.ExecContext(ctx, "SELECT id FROM config WHERE gid = $1 FOR UPDATE", c.GID())
21✔
200
        if err != nil && err.Error() != "sql: no rows in result set" {
21✔
NEW
201
                return fmt.Errorf("failed to lock row: %w", err)
×
NEW
202
        }
×
203

204
        // execute the main query
205
        _, err = tx.ExecContext(ctx, query, c.GID(), data, time.Now())
21✔
206
        if err != nil {
21✔
NEW
207
                return fmt.Errorf("failed to set config: %w", err)
×
NEW
208
        }
×
209

210
        // commit the transaction
211
        if err = tx.Commit(); err != nil {
21✔
NEW
212
                return fmt.Errorf("failed to commit transaction: %w", err)
×
NEW
213
        }
×
214

215
        return nil
21✔
216
}
217

218
// LastUpdated returns the last update time of the configuration
219
func (c *Config[T]) LastUpdated(ctx context.Context) (time.Time, error) {
8✔
220
        c.RLock()
8✔
221
        defer c.RUnlock()
8✔
222

8✔
223
        var record struct {
8✔
224
                UpdatedAt time.Time `db:"updated_at"`
8✔
225
        }
8✔
226
        query := c.Adopt("SELECT updated_at FROM config WHERE gid = ?")
8✔
227
        err := c.GetContext(ctx, &record, query, c.GID())
8✔
228
        if err != nil {
12✔
229
                if err.Error() == "sql: no rows in result set" {
8✔
230
                        return time.Time{}, fmt.Errorf("no configuration found")
4✔
231
                }
4✔
NEW
232
                return time.Time{}, fmt.Errorf("failed to get config update time: %w", err)
×
233
        }
234
        return record.UpdatedAt, nil
4✔
235
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc