mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Attach Blossom auth token in clients
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
9f2a11b338
commit
5cc7e4493e
@@ -116,6 +116,8 @@ pub struct BuzzClient {
|
||||
auth_tag: Option<Tag>,
|
||||
/// Raw JSON of the auth tag for the `x-auth-tag` HTTP header.
|
||||
auth_tag_json: Option<String>,
|
||||
/// API token attached as `X-Auth-Token` for Blossom media uploads.
|
||||
api_token: Option<String>,
|
||||
}
|
||||
|
||||
impl BuzzClient {
|
||||
@@ -124,6 +126,7 @@ impl BuzzClient {
|
||||
keys: Keys,
|
||||
auth_tag: Option<Tag>,
|
||||
auth_tag_json: Option<String>,
|
||||
api_token: Option<String>,
|
||||
) -> Result<Self, CliError> {
|
||||
let http = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
@@ -136,6 +139,11 @@ impl BuzzClient {
|
||||
keys,
|
||||
auth_tag,
|
||||
auth_tag_json,
|
||||
api_token: api_token
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.is_empty())
|
||||
.map(str::to_string),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -375,13 +383,16 @@ impl BuzzClient {
|
||||
Duration::from_secs(120)
|
||||
};
|
||||
let url = format!("{}/media/upload", self.relay_url);
|
||||
let req = self
|
||||
let mut req = self
|
||||
.http
|
||||
.put(&url)
|
||||
.timeout(upload_timeout)
|
||||
.header("Authorization", &auth_header)
|
||||
.header("Content-Type", &mime)
|
||||
.header("X-SHA-256", &sha256);
|
||||
if let Some(token) = self.api_token.as_deref() {
|
||||
req = req.header("X-Auth-Token", token);
|
||||
}
|
||||
|
||||
let resp = self.with_auth_tag(req).body(bytes).send().await?;
|
||||
if !resp.status().is_success() {
|
||||
|
||||
@@ -842,7 +842,7 @@ mod tests {
|
||||
let keys =
|
||||
nostr::Keys::parse("0000000000000000000000000000000000000000000000000000000000000001")
|
||||
.expect("valid test key");
|
||||
BuzzClient::new("ws://localhost:3000".to_string(), keys, None, None)
|
||||
BuzzClient::new("ws://localhost:3000".to_string(), keys, None, None, None)
|
||||
.expect("client construction should not fail")
|
||||
}
|
||||
|
||||
|
||||
@@ -786,7 +786,7 @@ mod tests {
|
||||
// common quick-check inputs.
|
||||
|
||||
fn test_client(keys: nostr::Keys) -> BuzzClient {
|
||||
BuzzClient::new("http://127.0.0.1:9".into(), keys, None, None).unwrap()
|
||||
BuzzClient::new("http://127.0.0.1:9".into(), keys, None, None, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -56,6 +56,7 @@ Configuration (flags override env vars):
|
||||
BUZZ_RELAY_URL Relay base URL [default: http://localhost:3000]
|
||||
BUZZ_PRIVATE_KEY Nostr private key (hex or nsec) [required]
|
||||
BUZZ_AUTH_TAG NIP-OA auth tag JSON [optional]
|
||||
BUZZ_API_TOKEN API token for Blossom media upload [optional]
|
||||
|
||||
The 'pack' subcommand runs locally and does not require a relay connection.
|
||||
|
||||
@@ -75,6 +76,10 @@ struct Cli {
|
||||
#[arg(long, env = "BUZZ_AUTH_TAG")]
|
||||
auth_tag: Option<String>,
|
||||
|
||||
/// API token attached as X-Auth-Token for Blossom media uploads.
|
||||
#[arg(long, env = "BUZZ_API_TOKEN")]
|
||||
api_token: Option<String>,
|
||||
|
||||
/// Output format: 'json' (default, full fields) or 'compact' (reduced fields).
|
||||
#[arg(long, value_enum, default_value = "json")]
|
||||
format: OutputFormat,
|
||||
@@ -1420,7 +1425,9 @@ async fn run(cli: Cli) -> Result<(), CliError> {
|
||||
}
|
||||
|
||||
// Auth: private key is required for all relay operations.
|
||||
// The keypair IS the identity — no tokens, no other auth.
|
||||
// The keypair is the identity for Nostr/WebSocket auth. `BUZZ_API_TOKEN`,
|
||||
// when present, is only attached to Blossom media uploads as an additional
|
||||
// relay-side scope credential.
|
||||
let private_key_str = cli.private_key.ok_or_else(|| {
|
||||
CliError::Auth("BUZZ_PRIVATE_KEY is required (use --private-key or set env var)".into())
|
||||
})?;
|
||||
@@ -1443,7 +1450,13 @@ async fn run(cli: Cli) -> Result<(), CliError> {
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let client = BuzzClient::new(relay_url, keys, auth_tag, auth_tag_json)?;
|
||||
let api_token = cli
|
||||
.api_token
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.is_empty())
|
||||
.map(str::to_string);
|
||||
let client = BuzzClient::new(relay_url, keys, auth_tag, auth_tag_json, api_token)?;
|
||||
|
||||
match cli.command {
|
||||
Cmd::Messages(sub) => commands::messages::dispatch(sub, &client, &cli.format).await,
|
||||
@@ -1523,6 +1536,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_token_flag_is_available() {
|
||||
let cmd = Cli::command();
|
||||
let api_token = cmd
|
||||
.get_arguments()
|
||||
.find(|arg| arg.get_id() == "api_token")
|
||||
.expect("global api_token argument should exist");
|
||||
|
||||
assert_eq!(api_token.get_long(), Some("api-token"));
|
||||
assert!(api_token
|
||||
.get_env()
|
||||
.is_some_and(|env| env == "BUZZ_API_TOKEN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subcommand_names_are_stable() {
|
||||
fn names(cmd: &clap::Command, group: &str) -> Vec<String> {
|
||||
|
||||
@@ -15,9 +15,8 @@ use crate::managed_agents::ManagedAgentProcess;
|
||||
pub struct AppState {
|
||||
pub keys: Mutex<Keys>,
|
||||
pub http_client: reqwest::Client,
|
||||
/// Workspace-provided relay URL override. Set by `apply_workspace` on app
|
||||
/// init and takes priority over env vars and compile-time defaults.
|
||||
pub relay_url_override: Mutex<Option<String>>,
|
||||
pub auth_token_override: Mutex<Option<String>>,
|
||||
pub managed_agents_store_lock: Mutex<()>,
|
||||
pub channel_templates_store_lock: Mutex<()>,
|
||||
pub managed_agent_processes: Mutex<HashMap<String, ManagedAgentProcess>>,
|
||||
@@ -93,6 +92,7 @@ pub fn build_app_state() -> AppState {
|
||||
.build()
|
||||
.unwrap_or_else(|_| reqwest::Client::new()),
|
||||
relay_url_override: Mutex::new(None),
|
||||
auth_token_override: Mutex::new(None),
|
||||
managed_agents_store_lock: Mutex::new(()),
|
||||
channel_templates_store_lock: Mutex::new(()),
|
||||
managed_agent_processes: Mutex::new(HashMap::new()),
|
||||
|
||||
@@ -7,7 +7,7 @@ use tauri::State;
|
||||
use crate::app_state::AppState;
|
||||
use crate::relay::{
|
||||
classify_request_error, parse_json_response, relay_api_base_url_with_override,
|
||||
relay_error_message,
|
||||
relay_error_message, workspace_auth_token,
|
||||
};
|
||||
|
||||
use super::media_transcode::{
|
||||
@@ -216,12 +216,15 @@ async fn do_upload(
|
||||
"Nostr {}",
|
||||
URL_SAFE_NO_PAD.encode(auth_event.as_json().as_bytes())
|
||||
);
|
||||
let req = state
|
||||
let mut req = state
|
||||
.http_client
|
||||
.put(format!("{base_url}/media/upload"))
|
||||
.header("Authorization", &auth_header)
|
||||
.header("Content-Type", mime)
|
||||
.header("X-SHA-256", &sha256);
|
||||
if let Some(token) = workspace_auth_token(state) {
|
||||
req = req.header("X-Auth-Token", token);
|
||||
}
|
||||
|
||||
// With a progress channel, stream the body in chunks and emit a
|
||||
// `media-upload-progress` event as each chunk is handed to the socket,
|
||||
|
||||
@@ -15,7 +15,9 @@ use tokio_util::sync::CancellationToken;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::relay::{relay_api_base_url_with_override, relay_ws_url_with_override};
|
||||
use crate::relay::{
|
||||
relay_api_base_url_with_override, relay_ws_url_with_override, workspace_auth_token,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct PairingSasPayload {
|
||||
@@ -107,11 +109,14 @@ pub async fn start_pairing(
|
||||
let (session, qr_payload) = PairingSession::new_source(qr_relay_url);
|
||||
let qr_uri = encode_qr(&qr_payload);
|
||||
|
||||
let payload_json = serde_json::json!({
|
||||
let mut payload_json = serde_json::json!({
|
||||
"relayUrl": http_url,
|
||||
"pubkey": pubkey_hex,
|
||||
"nsec": nsec,
|
||||
});
|
||||
if let Some(token) = workspace_auth_token(&state) {
|
||||
payload_json["token"] = serde_json::Value::String(token);
|
||||
}
|
||||
|
||||
{
|
||||
let mut s = pairing.session.lock().await;
|
||||
|
||||
@@ -60,6 +60,7 @@ pub fn validate_repos_dir(dir: String) -> Result<(), String> {
|
||||
pub fn apply_workspace(
|
||||
relay_url: String,
|
||||
nsec: Option<String>,
|
||||
token: Option<String>,
|
||||
repos_dir: Option<String>,
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
@@ -97,6 +98,17 @@ pub fn apply_workspace(
|
||||
let mut override_guard = state.relay_url_override.lock().map_err(|e| e.to_string())?;
|
||||
*override_guard = Some(relay_url);
|
||||
}
|
||||
{
|
||||
let mut token_guard = state
|
||||
.auth_token_override
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
*token_guard = token
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string);
|
||||
}
|
||||
|
||||
if let Some(keys) = parsed_keys {
|
||||
let mut keys_guard = state.keys.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -39,6 +39,18 @@ fn workspace_relay_override(state: &AppState) -> Option<String> {
|
||||
.and_then(|guard| guard.clone())
|
||||
}
|
||||
|
||||
/// Read the workspace API/Blossom auth token override, if set. Returns `None`
|
||||
/// when no trimmed token is active or when the mutex is poisoned (best-effort).
|
||||
pub fn workspace_auth_token(state: &AppState) -> Option<String> {
|
||||
state.auth_token_override.lock().ok().and_then(|guard| {
|
||||
guard
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the relay WebSocket URL, checking the workspace override first.
|
||||
/// Precedence: workspace override > env vars > build-time vars > default.
|
||||
pub fn relay_ws_url_with_override(state: &AppState) -> String {
|
||||
|
||||
@@ -420,6 +420,7 @@ class PairingNotifier extends Notifier<PairingState> {
|
||||
final relayUrl = data['relayUrl'] as String?;
|
||||
final pubkey = data['pubkey'] as String?;
|
||||
final nsec = data['nsec'] as String?;
|
||||
final token = data['token'] as String?;
|
||||
|
||||
if (relayUrl == null) {
|
||||
throw const FormatException('Missing relayUrl in payload');
|
||||
@@ -440,6 +441,7 @@ class PairingNotifier extends Notifier<PairingState> {
|
||||
relayUrl: relayUrl,
|
||||
pubkey: pubkey,
|
||||
nsec: nsec,
|
||||
token: token,
|
||||
);
|
||||
await ref
|
||||
.read(authProvider.notifier)
|
||||
@@ -616,6 +618,7 @@ class PairingNotifier extends Notifier<PairingState> {
|
||||
relayUrl: relayUrl,
|
||||
pubkey: decoded['pubkey'] as String?,
|
||||
nsec: decoded['nsec'] as String?,
|
||||
token: decoded['token'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class MediaUploadService {
|
||||
DateTime Function()? now,
|
||||
http.Client? httpClient,
|
||||
}) : _baseUrl = baseUrl,
|
||||
_apiToken = apiToken,
|
||||
_apiToken = apiToken?.trim(),
|
||||
_nsec = nsec,
|
||||
_pickGalleryImage = pickGalleryImage,
|
||||
_pickGalleryVideo = pickGalleryVideo,
|
||||
@@ -641,7 +641,7 @@ final mediaUploadServiceProvider = Provider<MediaUploadService>((ref) {
|
||||
final picker = ImagePicker();
|
||||
final service = MediaUploadService(
|
||||
baseUrl: config.baseUrl,
|
||||
apiToken: null,
|
||||
apiToken: config.apiToken,
|
||||
nsec: config.nsec,
|
||||
pickGalleryImage: () => picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
|
||||
@@ -15,7 +15,10 @@ class RelayConfig {
|
||||
/// Nostr secret key (bech32 nsec) for signing events and NIP-42 AUTH.
|
||||
final String? nsec;
|
||||
|
||||
const RelayConfig({required this.baseUrl, this.nsec});
|
||||
/// API token attached as X-Auth-Token for Blossom media upload.
|
||||
final String? apiToken;
|
||||
|
||||
const RelayConfig({required this.baseUrl, this.nsec, this.apiToken});
|
||||
|
||||
/// Derive the websocket URL from the HTTP base URL.
|
||||
String get wsUrl {
|
||||
@@ -46,15 +49,19 @@ class RelayConfigNotifier extends Notifier<RelayConfig> {
|
||||
final activeAsync = ref.watch(activeWorkspaceProvider);
|
||||
final active = activeAsync.value;
|
||||
if (active != null) {
|
||||
return RelayConfig(baseUrl: active.relayUrl, nsec: active.nsec);
|
||||
return RelayConfig(
|
||||
baseUrl: active.relayUrl,
|
||||
nsec: active.nsec,
|
||||
apiToken: active.token,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to compile-time env config (dev mode).
|
||||
return const RelayConfig(baseUrl: Env.relayUrl);
|
||||
return const RelayConfig(baseUrl: Env.relayUrl, apiToken: null);
|
||||
}
|
||||
|
||||
void update({required String baseUrl, String? nsec}) {
|
||||
state = RelayConfig(baseUrl: baseUrl, nsec: nsec);
|
||||
void update({required String baseUrl, String? nsec, String? apiToken}) {
|
||||
state = RelayConfig(baseUrl: baseUrl, nsec: nsec, apiToken: apiToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ class Workspace {
|
||||
final String relayUrl;
|
||||
final String? pubkey;
|
||||
final String? nsec;
|
||||
final String? token;
|
||||
final DateTime addedAt;
|
||||
|
||||
const Workspace({
|
||||
@@ -17,6 +18,7 @@ class Workspace {
|
||||
required this.relayUrl,
|
||||
this.pubkey,
|
||||
this.nsec,
|
||||
this.token,
|
||||
required this.addedAt,
|
||||
});
|
||||
|
||||
@@ -25,6 +27,7 @@ class Workspace {
|
||||
required String relayUrl,
|
||||
String? pubkey,
|
||||
String? nsec,
|
||||
String? token,
|
||||
}) {
|
||||
return Workspace(
|
||||
id: _uuid.v4(),
|
||||
@@ -32,6 +35,7 @@ class Workspace {
|
||||
relayUrl: relayUrl,
|
||||
pubkey: pubkey,
|
||||
nsec: nsec,
|
||||
token: _normalizeToken(token),
|
||||
addedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
@@ -41,6 +45,7 @@ class Workspace {
|
||||
String? relayUrl,
|
||||
Object? pubkey = _sentinel,
|
||||
Object? nsec = _sentinel,
|
||||
Object? token = _sentinel,
|
||||
}) {
|
||||
return Workspace(
|
||||
id: id,
|
||||
@@ -48,6 +53,9 @@ class Workspace {
|
||||
relayUrl: relayUrl ?? this.relayUrl,
|
||||
pubkey: pubkey == _sentinel ? this.pubkey : pubkey as String?,
|
||||
nsec: nsec == _sentinel ? this.nsec : nsec as String?,
|
||||
token: token == _sentinel
|
||||
? this.token
|
||||
: _normalizeToken(token as String?),
|
||||
addedAt: addedAt,
|
||||
);
|
||||
}
|
||||
@@ -58,6 +66,7 @@ class Workspace {
|
||||
'relayUrl': relayUrl,
|
||||
if (pubkey != null) 'pubkey': pubkey,
|
||||
if (nsec != null) 'nsec': nsec,
|
||||
if (token != null) 'token': token,
|
||||
'addedAt': addedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
@@ -67,9 +76,15 @@ class Workspace {
|
||||
relayUrl: json['relayUrl'] as String,
|
||||
pubkey: json['pubkey'] as String?,
|
||||
nsec: json['nsec'] as String?,
|
||||
token: _normalizeToken(json['token'] as String?),
|
||||
addedAt: DateTime.parse(json['addedAt'] as String),
|
||||
);
|
||||
|
||||
static String? _normalizeToken(String? token) {
|
||||
final trimmed = token?.trim();
|
||||
return trimmed == null || trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
|
||||
/// Derive a human-friendly workspace name from a relay URL.
|
||||
static String nameFromUrl(String url) {
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,7 @@ class WorkspaceListNotifier extends AsyncNotifier<List<Workspace>> {
|
||||
final updated = existing.copyWith(
|
||||
pubkey: workspace.pubkey,
|
||||
nsec: workspace.nsec,
|
||||
token: workspace.token ?? existing.token,
|
||||
);
|
||||
await storage.save(updated);
|
||||
final updatedList = [...current];
|
||||
|
||||
@@ -43,6 +43,7 @@ class WorkspaceStorage {
|
||||
relayUrl: legacyUrl,
|
||||
pubkey: legacyPubkey,
|
||||
nsec: legacyNsec,
|
||||
token: legacyToken,
|
||||
);
|
||||
|
||||
await _saveList([workspace]);
|
||||
|
||||
@@ -355,11 +355,18 @@ class _FakeRelayConfigNotifier extends RelayConfigNotifier {
|
||||
_FakeRelayConfigNotifier({required String? nsec}) : _nsec = nsec;
|
||||
|
||||
@override
|
||||
RelayConfig build() =>
|
||||
RelayConfig(baseUrl: 'http://localhost:3000', nsec: _nsec);
|
||||
RelayConfig build() => RelayConfig(
|
||||
baseUrl: 'http://localhost:3000',
|
||||
nsec: _nsec,
|
||||
apiToken: null,
|
||||
);
|
||||
|
||||
void setNsec(String? nsec) {
|
||||
_nsec = nsec;
|
||||
state = RelayConfig(baseUrl: 'http://localhost:3000', nsec: _nsec);
|
||||
state = RelayConfig(
|
||||
baseUrl: 'http://localhost:3000',
|
||||
nsec: _nsec,
|
||||
apiToken: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ String _encodePairingCode({
|
||||
String relayUrl = 'http://test:3000',
|
||||
String? pubkey,
|
||||
String? nsec,
|
||||
String? token,
|
||||
}) {
|
||||
final json = <String, dynamic>{
|
||||
'relayUrl': relayUrl,
|
||||
@@ -164,6 +165,8 @@ String _encodePairingCode({
|
||||
if (pubkey != null) 'pubkey': pubkey,
|
||||
// ignore: use_null_aware_elements
|
||||
if (nsec != null) 'nsec': nsec,
|
||||
// ignore: use_null_aware_elements
|
||||
if (token != null) 'token': token,
|
||||
};
|
||||
return base64Url.encode(utf8.encode(jsonEncode(json)));
|
||||
}
|
||||
|
||||
@@ -278,6 +278,7 @@ void main() {
|
||||
);
|
||||
expect(capturedRequest!.headers['Content-Type'], 'image/png');
|
||||
expect(capturedRequest!.headers['X-SHA-256'], isNotEmpty);
|
||||
expect(capturedRequest!.headers['X-Auth-Token'], isNull);
|
||||
expect(capturedRequest!.bodyBytes, _pngBytes);
|
||||
|
||||
final authHeader = capturedRequest!.headers['Authorization'];
|
||||
@@ -308,6 +309,41 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('attaches configured API token to uploads', () async {
|
||||
final keychain = nostr.Keys.generate();
|
||||
|
||||
http.Request? capturedRequest;
|
||||
final client = http_testing.MockClient((request) async {
|
||||
capturedRequest = request;
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'url': 'https://relay.example/media/test.png',
|
||||
'sha256':
|
||||
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
'size': 16,
|
||||
'type': 'image/png',
|
||||
'uploaded': 1,
|
||||
}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final service = MediaUploadService(
|
||||
baseUrl: 'https://relay.example',
|
||||
apiToken: 'buzz_media_token',
|
||||
nsec: keychain.nsec,
|
||||
httpClient: client,
|
||||
pickGalleryVideo: () async => null,
|
||||
pickGalleryImage: () async =>
|
||||
XFile.fromData(_pngBytes, name: 'tiny.png'),
|
||||
);
|
||||
|
||||
await service.pickAndUploadImage();
|
||||
|
||||
expect(capturedRequest, isNotNull);
|
||||
expect(capturedRequest!.headers['X-Auth-Token'], 'buzz_media_token');
|
||||
});
|
||||
|
||||
test('returns null when the gallery picker is cancelled', () async {
|
||||
final service = MediaUploadService(
|
||||
baseUrl: 'https://relay.example',
|
||||
|
||||
@@ -101,6 +101,62 @@ void main() {
|
||||
final activeId = await workspaceStorage.loadActiveId();
|
||||
expect(activeId, ws2.id);
|
||||
});
|
||||
|
||||
test(
|
||||
'updates duplicate workspace without clearing existing token',
|
||||
() async {
|
||||
container = createContainer();
|
||||
await container.read(workspaceListProvider.future);
|
||||
|
||||
final original = Workspace.create(
|
||||
name: 'Test',
|
||||
relayUrl: 'https://test.example.com',
|
||||
pubkey: 'old-pubkey',
|
||||
nsec: 'old-nsec',
|
||||
token: 'buzz_existing',
|
||||
);
|
||||
final duplicate = Workspace.create(
|
||||
name: 'Test',
|
||||
relayUrl: 'https://test.example.com',
|
||||
pubkey: 'new-pubkey',
|
||||
nsec: 'new-nsec',
|
||||
);
|
||||
|
||||
final notifier = container.read(workspaceListProvider.notifier);
|
||||
await notifier.addWorkspace(original);
|
||||
await notifier.addWorkspace(duplicate);
|
||||
|
||||
final workspaces = await container.read(workspaceListProvider.future);
|
||||
expect(workspaces, hasLength(1));
|
||||
expect(workspaces.first.pubkey, 'new-pubkey');
|
||||
expect(workspaces.first.nsec, 'new-nsec');
|
||||
expect(workspaces.first.token, 'buzz_existing');
|
||||
},
|
||||
);
|
||||
|
||||
test('duplicate workspace updates token when provided', () async {
|
||||
container = createContainer();
|
||||
await container.read(workspaceListProvider.future);
|
||||
|
||||
final original = Workspace.create(
|
||||
name: 'Test',
|
||||
relayUrl: 'https://test.example.com',
|
||||
token: 'buzz_existing',
|
||||
);
|
||||
final duplicate = Workspace.create(
|
||||
name: 'Test',
|
||||
relayUrl: 'https://test.example.com',
|
||||
token: 'buzz_new',
|
||||
);
|
||||
|
||||
final notifier = container.read(workspaceListProvider.notifier);
|
||||
await notifier.addWorkspace(original);
|
||||
await notifier.addWorkspace(duplicate);
|
||||
|
||||
final workspaces = await container.read(workspaceListProvider.future);
|
||||
expect(workspaces, hasLength(1));
|
||||
expect(workspaces.first.token, 'buzz_new');
|
||||
});
|
||||
});
|
||||
|
||||
group('activeWorkspaceProvider', () {
|
||||
|
||||
@@ -176,6 +176,7 @@ void main() {
|
||||
expect(loaded.first.relayUrl, 'https://legacy.example.com');
|
||||
expect(loaded.first.pubkey, 'legacy_pub');
|
||||
expect(loaded.first.nsec, 'legacy_nsec');
|
||||
expect(loaded.first.token, 'legacy_token');
|
||||
expect(loaded.first.name, isNotEmpty);
|
||||
|
||||
// Legacy keys should be deleted.
|
||||
|
||||
Reference in New Issue
Block a user