Prepara el terreno para que el cliente cargue su propia cuenta de IA sin que su clave quede en texto plano ni que el selector le muestre las de los demás. - ClaveEnClaro() descifra con fallback a texto plano: las filas viejas se leen igual y quedan cifradas al primer guardado, sin script ni downtime. utils.Decrypt hace panic (no devuelve error) con entrada que no es un ciphertext válido, así que el fallback va sobre recover — eso mismo es el mecanismo de detección de "todavía está en claro". - Migrados TODOS los lectores: uMind (chat y embeddings), bot de Telegram, Whisper, Query Runner, streaming de IA y Landing Generator. Un lector sin migrar mandaría el ciphertext como API key. - AiConfig gana TenantID (null = global del staff) y GetAiConfigSelectPorTenants para acotar el selector. - Test de los 4 casos del fallback, incluido hex válido que no descifra. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
63 lines
2.2 KiB
Go
63 lines
2.2 KiB
Go
package models
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/config"
|
|
"github.com/sujit-baniya/fiber-boilerplate/utils"
|
|
)
|
|
|
|
// El resto del proyecto inicializa app.Http en el arranque; en tests hay que
|
|
// hacerlo a mano antes de tocar cualquier cosa que lea la config.
|
|
func conAppKey(t *testing.T, key string) {
|
|
t.Helper()
|
|
anterior := app.Http
|
|
app.Http = &config.AppConfig{}
|
|
app.Http.Server.Key = key
|
|
t.Cleanup(func() { app.Http = anterior })
|
|
}
|
|
|
|
// La migración progresiva de AiConfig.ApiKey depende de que ClaveEnClaro
|
|
// distinga una clave cifrada de una que todavía está en texto plano. Si esto
|
|
// se rompe, el sistema empieza a mandar ciphertext como API key a OpenAI y
|
|
// todos los agentes dejan de responder.
|
|
func TestClaveEnClaroMigracionProgresiva(t *testing.T) {
|
|
conAppKey(t, "6368616e676520746869732070617373776f726420746f206120736563726574") // 32 bytes en hex
|
|
|
|
casos := []struct {
|
|
nombre string
|
|
guardado string
|
|
esperado string
|
|
descripci string
|
|
}{
|
|
{"vacío", "", "", "sin clave no hay nada que descifrar"},
|
|
{"texto plano", "sk-proj-abc123", "sk-proj-abc123", "fila vieja sin cifrar, se devuelve tal cual"},
|
|
{"cifrada", utils.Encrypt("sk-proj-abc123", app.Http.Server.Key), "sk-proj-abc123", "fila nueva, se descifra"},
|
|
{"hex que no es ciphertext", "deadbeef", "deadbeef", "hex válido pero no descifrable: no debe romper"},
|
|
}
|
|
|
|
for _, cas := range casos {
|
|
t.Run(cas.nombre, func(t *testing.T) {
|
|
cfg := &AiConfig{ApiKey: cas.guardado}
|
|
if got := cfg.ClaveEnClaro(); got != cas.esperado {
|
|
t.Errorf("%s: ClaveEnClaro() = %q, esperaba %q", cas.descripci, got, cas.esperado)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sin APP_KEY el guardado no debe romperse: se guarda en claro y se lee en
|
|
// claro, que es exactamente el comportamiento previo a esta migración.
|
|
func TestCifrarClaveAiSinAppKey(t *testing.T) {
|
|
conAppKey(t, "")
|
|
|
|
if got := CifrarClaveAi("sk-test"); got != "sk-test" {
|
|
t.Errorf("CifrarClaveAi sin APP_KEY = %q, esperaba pasarla tal cual", got)
|
|
}
|
|
cfg := &AiConfig{ApiKey: "sk-test"}
|
|
if got := cfg.ClaveEnClaro(); got != "sk-test" {
|
|
t.Errorf("ClaveEnClaro sin APP_KEY = %q, esperaba pasarla tal cual", got)
|
|
}
|
|
}
|