feat(dev-variables): 项目开发变量与测试账户补给(V1–V11) #23
@ -1,6 +1,6 @@
|
||||
//! 项目级开发变量本体。SQLite 只保存元数据和随机引用;值仅驻留受保护存储。
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::credential_store::{
|
||||
delete_references, put_verified, restore_references, CredentialStore,
|
||||
@ -11,10 +11,22 @@ pub struct DevVariableStatus {
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub purpose: String,
|
||||
pub kind: String,
|
||||
pub service: Option<String>,
|
||||
pub environment: Option<String>,
|
||||
pub configured: bool,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TestAccountValue {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
const SCALAR_KIND: &str = "scalar";
|
||||
const TEST_ACCOUNT_KIND: &str = "test_account";
|
||||
|
||||
fn normalized_name(name: &str) -> Result<String, String> {
|
||||
let name = name.trim().to_ascii_uppercase();
|
||||
if name.is_empty()
|
||||
@ -52,6 +64,34 @@ fn reference(conn: &Connection, project_id: &str, name: &str) -> Result<Option<S
|
||||
.map_err(|_| "开发变量未登记".to_string())
|
||||
}
|
||||
|
||||
fn require_kind(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
expected_kind: &str,
|
||||
) -> Result<(), String> {
|
||||
let kind: String = conn
|
||||
.query_row(
|
||||
"SELECT kind FROM project_dev_variables WHERE project_id=?1 AND name=?2",
|
||||
params![project_id, name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|_| "开发需求未登记".to_string())?;
|
||||
if kind != expected_kind {
|
||||
return Err("开发需求类型不匹配".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn metadata(value: &str, label: &str, max_chars: usize) -> Result<String, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() || value.chars().count() > max_chars || value.chars().any(char::is_control)
|
||||
{
|
||||
return Err(format!("{label}须为 1–{max_chars} 字且不含控制字符"));
|
||||
}
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
@ -60,16 +100,58 @@ pub fn register(
|
||||
) -> Result<(), String> {
|
||||
require_project(conn, project_id)?;
|
||||
let name = normalized_name(name)?;
|
||||
let purpose = purpose.trim();
|
||||
if purpose.is_empty() || purpose.chars().count() > 240 || purpose.chars().any(char::is_control)
|
||||
{
|
||||
return Err("用途说明须为 1–240 字且不含控制字符".into());
|
||||
}
|
||||
let purpose = metadata(purpose, "用途说明", 240)?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO project_dev_variables (project_id,name,purpose) VALUES (?1,?2,?3)",
|
||||
params![project_id, name, purpose],
|
||||
)
|
||||
.map_err(|_| "登记开发变量失败".to_string())?;
|
||||
require_kind(conn, project_id, &name, SCALAR_KIND)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register_test_account(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
service: &str,
|
||||
environment: &str,
|
||||
purpose: &str,
|
||||
) -> Result<(), String> {
|
||||
require_project(conn, project_id)?;
|
||||
let name = normalized_name(name)?;
|
||||
let service = metadata(service, "目标系统", 80)?;
|
||||
let environment = metadata(environment, "测试环境", 80)?;
|
||||
if matches!(
|
||||
environment.to_ascii_lowercase().as_str(),
|
||||
"prod" | "production" | "live"
|
||||
) || environment.contains("生产")
|
||||
{
|
||||
return Err("测试账户不得登记生产环境".into());
|
||||
}
|
||||
let purpose = metadata(purpose, "用途说明", 240)?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO project_dev_variables
|
||||
(project_id,name,purpose,kind,service,environment)
|
||||
VALUES (?1,?2,?3,'test_account',?4,?5)",
|
||||
params![project_id, name, purpose, service, environment],
|
||||
)
|
||||
.map_err(|_| "登记测试账户需求失败".to_string())?;
|
||||
let existing: (String, String, Option<String>, Option<String>) = conn
|
||||
.query_row(
|
||||
"SELECT kind,purpose,service,environment FROM project_dev_variables
|
||||
WHERE project_id=?1 AND name=?2",
|
||||
params![project_id, name],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)
|
||||
.map_err(|_| "查询测试账户需求失败".to_string())?;
|
||||
if existing.0 != TEST_ACCOUNT_KIND
|
||||
|| existing.1 != purpose
|
||||
|| existing.2.as_deref() != Some(service.as_str())
|
||||
|| existing.3.as_deref() != Some(environment.as_str())
|
||||
{
|
||||
return Err("同名需求的类型或用途不同,请更换名称".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -81,7 +163,7 @@ pub fn list(
|
||||
require_project(conn, project_id)?;
|
||||
let mut statement = conn
|
||||
.prepare(
|
||||
"SELECT name,purpose,credential_ref,updated_at FROM project_dev_variables
|
||||
"SELECT name,purpose,kind,service,environment,credential_ref,updated_at FROM project_dev_variables
|
||||
WHERE project_id=?1 ORDER BY name",
|
||||
)
|
||||
.map_err(|_| "查询开发变量失败".to_string())?;
|
||||
@ -90,23 +172,31 @@ pub fn list(
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, Option<String>>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, Option<String>>(3)?,
|
||||
row.get::<_, Option<String>>(4)?,
|
||||
row.get::<_, Option<String>>(5)?,
|
||||
row.get::<_, String>(6)?,
|
||||
))
|
||||
})
|
||||
.map_err(|_| "查询开发变量失败".to_string())?;
|
||||
let mut statuses = Vec::new();
|
||||
for row in rows {
|
||||
let (name, purpose, reference, updated_at) =
|
||||
let (name, purpose, kind, service, environment, reference, updated_at) =
|
||||
row.map_err(|_| "查询开发变量失败".to_string())?;
|
||||
let configured = match reference {
|
||||
Some(reference) => {
|
||||
store
|
||||
let bytes = store
|
||||
.get(&reference)
|
||||
.map_err(|_| "开发变量状态 needs_repair:受保护存储不可用".to_string())?
|
||||
.ok_or_else(|| "开发变量状态 needs_repair:凭证引用缺失".to_string())?
|
||||
.len()
|
||||
> 0
|
||||
.ok_or_else(|| "开发变量状态 needs_repair:凭证引用缺失".to_string())?;
|
||||
if kind == TEST_ACCOUNT_KIND {
|
||||
serde_json::from_slice::<TestAccountValue>(&bytes)
|
||||
.map(|value| !value.username.is_empty() && !value.password.is_empty())
|
||||
.map_err(|_| "测试账户状态 needs_repair:值格式无效".to_string())?
|
||||
} else {
|
||||
!bytes.is_empty()
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
@ -114,6 +204,9 @@ pub fn list(
|
||||
project_id: project_id.to_string(),
|
||||
name,
|
||||
purpose,
|
||||
kind,
|
||||
service,
|
||||
environment,
|
||||
configured,
|
||||
updated_at,
|
||||
});
|
||||
@ -126,9 +219,35 @@ pub fn read(
|
||||
store: &impl CredentialStore,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
) -> Result<String, String> {
|
||||
read_value(conn, store, project_id, name, SCALAR_KIND)
|
||||
}
|
||||
|
||||
pub fn read_test_account(
|
||||
conn: &Connection,
|
||||
store: &impl CredentialStore,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
) -> Result<TestAccountValue, String> {
|
||||
let value = read_value(conn, store, project_id, name, TEST_ACCOUNT_KIND)?;
|
||||
let account = serde_json::from_str::<TestAccountValue>(&value)
|
||||
.map_err(|_| "测试账户状态 needs_repair:值格式无效".to_string())?;
|
||||
if account.username.is_empty() || account.password.is_empty() {
|
||||
return Err("测试账户状态 needs_repair:字段缺失".into());
|
||||
}
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn read_value(
|
||||
conn: &Connection,
|
||||
store: &impl CredentialStore,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
expected_kind: &str,
|
||||
) -> Result<String, String> {
|
||||
require_project(conn, project_id)?;
|
||||
let name = normalized_name(name)?;
|
||||
require_kind(conn, project_id, &name, expected_kind)?;
|
||||
let reference =
|
||||
reference(conn, project_id, &name)?.ok_or_else(|| "开发变量尚未由用户填写".to_string())?;
|
||||
let bytes = store
|
||||
@ -144,9 +263,41 @@ pub fn save(
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
value: &str,
|
||||
) -> Result<(), String> {
|
||||
save_value(conn, store, project_id, name, value, SCALAR_KIND)
|
||||
}
|
||||
|
||||
pub fn save_test_account(
|
||||
conn: &Connection,
|
||||
store: &impl CredentialStore,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), String> {
|
||||
if username.is_empty() || password.is_empty() || username.len() > 1024 || password.len() > 1024
|
||||
{
|
||||
return Err("测试账号与密码均须为 1–1024 字节".into());
|
||||
}
|
||||
let value = serde_json::to_string(&TestAccountValue {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
})
|
||||
.map_err(|_| "序列化测试账户失败".to_string())?;
|
||||
save_value(conn, store, project_id, name, &value, TEST_ACCOUNT_KIND)
|
||||
}
|
||||
|
||||
fn save_value(
|
||||
conn: &Connection,
|
||||
store: &impl CredentialStore,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
value: &str,
|
||||
expected_kind: &str,
|
||||
) -> Result<(), String> {
|
||||
require_project(conn, project_id)?;
|
||||
let name = normalized_name(name)?;
|
||||
require_kind(conn, project_id, &name, expected_kind)?;
|
||||
if value.is_empty() || value.len() > 2560 {
|
||||
return Err("开发变量值长度须为 1–2560 字节".into());
|
||||
}
|
||||
@ -273,6 +424,26 @@ pub fn save_project_dev_variable(
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_project_test_account(
|
||||
project_id: String,
|
||||
name: String,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<(), String> {
|
||||
let conn = crate::db::pool()
|
||||
.get()
|
||||
.map_err(|_| "数据库不可用".to_string())?;
|
||||
save_test_account(
|
||||
&conn,
|
||||
&crate::credential_store::WindowsCredentialStore,
|
||||
&project_id,
|
||||
&name,
|
||||
&username,
|
||||
&password,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn revoke_project_dev_variable(project_id: String, name: String) -> Result<(), String> {
|
||||
let conn = crate::db::pool()
|
||||
@ -336,6 +507,137 @@ mod tests {
|
||||
assert!(read(&conn, &store, "p1", "E2E_PASSWORD").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_is_one_idempotent_request_and_one_protected_value() {
|
||||
let conn = fixture();
|
||||
let store = MemoryCredentialStore::new();
|
||||
register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"enterprise-system",
|
||||
"test",
|
||||
"登录验收",
|
||||
)
|
||||
.unwrap();
|
||||
register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"e2e_login",
|
||||
"enterprise-system",
|
||||
"test",
|
||||
"登录验收",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"another-system",
|
||||
"test",
|
||||
"登录验收"
|
||||
)
|
||||
.is_err());
|
||||
assert!(register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"PROD_LOGIN",
|
||||
"enterprise-system",
|
||||
"production",
|
||||
"登录验收"
|
||||
)
|
||||
.is_err());
|
||||
assert!(register(&conn, "p1", "E2E_LOGIN", "普通变量").is_err());
|
||||
let statuses = list(&conn, &store, "p1").unwrap();
|
||||
assert_eq!(statuses.len(), 1);
|
||||
assert_eq!(statuses[0].kind, TEST_ACCOUNT_KIND);
|
||||
assert_eq!(statuses[0].service.as_deref(), Some("enterprise-system"));
|
||||
assert!(!statuses[0].configured);
|
||||
save_test_account(
|
||||
&conn,
|
||||
&store,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"qa-only-user",
|
||||
"qa-only-password",
|
||||
)
|
||||
.unwrap();
|
||||
let status_json = serde_json::to_string(&list(&conn, &store, "p1").unwrap()).unwrap();
|
||||
assert!(!status_json.contains("qa-only-user"));
|
||||
assert!(!status_json.contains("qa-only-password"));
|
||||
assert!(read(&conn, &store, "p1", "E2E_LOGIN").is_err());
|
||||
assert!(save(&conn, &store, "p1", "E2E_LOGIN", "wrong-type").is_err());
|
||||
let account = read_test_account(&conn, &store, "p1", "E2E_LOGIN").unwrap();
|
||||
assert_eq!(account.username, "qa-only-user");
|
||||
assert_eq!(account.password, "qa-only-password");
|
||||
revoke(&conn, &store, "p1", "E2E_LOGIN").unwrap();
|
||||
assert!(read_test_account(&conn, &store, "p1", "E2E_LOGIN").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_test_account_overwrite_preserves_both_old_fields() {
|
||||
let conn = fixture();
|
||||
register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"enterprise-system",
|
||||
"test",
|
||||
"登录验收",
|
||||
)
|
||||
.unwrap();
|
||||
let old_ref = "dev-variable/qa-only-account-old";
|
||||
let store = MemoryCredentialStore::failing_delete_for(old_ref);
|
||||
save_test_account(&conn, &store, "p1", "E2E_LOGIN", "old-user", "old-password").unwrap();
|
||||
let generated_ref: String = conn
|
||||
.query_row(
|
||||
"SELECT credential_ref FROM project_dev_variables WHERE project_id='p1' AND name='E2E_LOGIN'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
store
|
||||
.put(old_ref, &store.get(&generated_ref).unwrap().unwrap())
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"UPDATE project_dev_variables SET credential_ref=?1 WHERE project_id='p1' AND name='E2E_LOGIN'",
|
||||
[old_ref],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
save_test_account(&conn, &store, "p1", "E2E_LOGIN", "new-user", "new-password")
|
||||
.is_err()
|
||||
);
|
||||
let account = read_test_account(&conn, &store, "p1", "E2E_LOGIN").unwrap();
|
||||
assert_eq!(account.username, "old-user");
|
||||
assert_eq!(account.password, "old-password");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_initial_test_account_write_stays_pending() {
|
||||
let conn = fixture();
|
||||
register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"enterprise-system",
|
||||
"test",
|
||||
"登录验收",
|
||||
)
|
||||
.unwrap();
|
||||
let store = MemoryCredentialStore::failing();
|
||||
assert!(save_test_account(&conn, &store, "p1", "E2E_LOGIN", "user", "password").is_err());
|
||||
assert!(!list(&conn, &MemoryCredentialStore::new(), "p1").unwrap()[0].configured);
|
||||
let reference: Option<String> = conn
|
||||
.query_row(
|
||||
"SELECT credential_ref FROM project_dev_variables WHERE project_id='p1' AND name='E2E_LOGIN'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(reference.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_write_or_delete_preserves_old_value_and_reference() {
|
||||
let conn = fixture();
|
||||
@ -455,6 +757,58 @@ mod tests {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_can_be_reused_after_database_reopen() {
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("lianjing-test-account-{}.db", uuid::Uuid::new_v4()));
|
||||
let store = MemoryCredentialStore::new();
|
||||
{
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
|
||||
crate::db::migrate(&conn);
|
||||
conn.execute(
|
||||
"INSERT INTO project_profiles (id,name) VALUES ('profile-1','QA')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO project_workspaces (id,profile_id) VALUES ('p1','profile-1')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
register_test_account(
|
||||
&conn,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"enterprise-system",
|
||||
"test",
|
||||
"登录验收",
|
||||
)
|
||||
.unwrap();
|
||||
save_test_account(
|
||||
&conn,
|
||||
&store,
|
||||
"p1",
|
||||
"E2E_LOGIN",
|
||||
"reused-user",
|
||||
"reused-password",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
{
|
||||
let conn = Connection::open(&path).unwrap();
|
||||
crate::db::migrate(&conn);
|
||||
let statuses = list(&conn, &store, "p1").unwrap();
|
||||
assert_eq!(statuses.len(), 1);
|
||||
assert!(statuses[0].configured);
|
||||
let account = read_test_account(&conn, &store, "p1", "E2E_LOGIN").unwrap();
|
||||
assert_eq!(account.username, "reused-user");
|
||||
assert_eq!(account.password, "reused-password");
|
||||
revoke(&conn, &store, "p1", "E2E_LOGIN").unwrap();
|
||||
}
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn tauri_commands_use_same_state_and_never_return_value_in_list() {
|
||||
@ -495,4 +849,54 @@ mod tests {
|
||||
conn.execute("DELETE FROM project_profiles WHERE id=?1", [&profile_id])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn tauri_test_account_command_uses_same_state_without_listing_values() {
|
||||
crate::db::init_test_pool();
|
||||
let project_id = format!("qa-only-account-{}", uuid::Uuid::new_v4());
|
||||
let profile_id = format!("qa-only-profile-{}", uuid::Uuid::new_v4());
|
||||
{
|
||||
let conn = crate::db::pool().get().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO project_profiles (id,name) VALUES (?1,'QA')",
|
||||
[&profile_id],
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO project_workspaces (id,profile_id) VALUES (?1,?2)",
|
||||
params![project_id, profile_id],
|
||||
)
|
||||
.unwrap();
|
||||
register_test_account(
|
||||
&conn,
|
||||
&project_id,
|
||||
"E2E_LOGIN",
|
||||
"enterprise-system",
|
||||
"test",
|
||||
"合成登录验收",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert!(!list_project_dev_variables(project_id.clone()).unwrap()[0].configured);
|
||||
save_project_test_account(
|
||||
project_id.clone(),
|
||||
"E2E_LOGIN".into(),
|
||||
"qa-only-command-user".into(),
|
||||
"qa-only-command-password".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let statuses = list_project_dev_variables(project_id.clone()).unwrap();
|
||||
assert!(statuses[0].configured);
|
||||
let serialized = serde_json::to_string(&statuses).unwrap();
|
||||
assert!(!serialized.contains("qa-only-command-user"));
|
||||
assert!(!serialized.contains("qa-only-command-password"));
|
||||
revoke_project_dev_variable(project_id.clone(), "E2E_LOGIN".into()).unwrap();
|
||||
assert!(!list_project_dev_variables(project_id.clone()).unwrap()[0].configured);
|
||||
let conn = crate::db::pool().get().unwrap();
|
||||
conn.execute("DELETE FROM project_workspaces WHERE id=?1", [&project_id])
|
||||
.unwrap();
|
||||
conn.execute("DELETE FROM project_profiles WHERE id=?1", [&profile_id])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1064,11 +1064,26 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
||||
name TEXT NOT NULL,
|
||||
purpose TEXT NOT NULL,
|
||||
credential_ref TEXT,
|
||||
kind TEXT NOT NULL DEFAULT 'scalar',
|
||||
service TEXT,
|
||||
environment TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (project_id, name)
|
||||
);",
|
||||
)
|
||||
.expect("migrate project dev variables");
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE project_dev_variables ADD COLUMN kind TEXT NOT NULL DEFAULT 'scalar'",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE project_dev_variables ADD COLUMN service TEXT",
|
||||
[],
|
||||
);
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE project_dev_variables ADD COLUMN environment TEXT",
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -1173,6 +1188,36 @@ mod tests {
|
||||
assert!(!columns.contains(&"value".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_columns_upgrade_existing_dev_variable_rows() {
|
||||
let conn = conn_with_schema();
|
||||
conn.execute_batch(
|
||||
"DROP TABLE project_dev_variables;
|
||||
CREATE TABLE project_dev_variables (
|
||||
project_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
purpose TEXT NOT NULL,
|
||||
credential_ref TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (project_id, name)
|
||||
);
|
||||
INSERT INTO project_dev_variables (project_id,name,purpose)
|
||||
VALUES ('p1','LEGACY_VALUE','legacy');",
|
||||
)
|
||||
.unwrap();
|
||||
migrate(&conn);
|
||||
migrate(&conn);
|
||||
let row: (String, Option<String>, Option<String>) = conn
|
||||
.query_row(
|
||||
"SELECT kind,service,environment FROM project_dev_variables
|
||||
WHERE project_id='p1' AND name='LEGACY_VALUE'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(row, ("scalar".to_string(), None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_state_touch_then_read() {
|
||||
let conn = conn_with_schema();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user