first pass and feature add ons
This commit is contained in:
@@ -6,12 +6,28 @@ public interface ICloudStateStore
|
||||
{
|
||||
AccountProfile GetAccount();
|
||||
DeviceRegistration GetRobot();
|
||||
RobotProfile GetRobotProfile();
|
||||
DeviceRegistration GetOrCreateDevice(string deviceId, string? firmwareVersion, string? applicationVersion);
|
||||
string IssueHubToken();
|
||||
string IssueRobotToken(string deviceId);
|
||||
CloudSession OpenSession(string kind, string? deviceId, string? token, string? hostName, string? path);
|
||||
CloudSession? FindSessionByToken(string token);
|
||||
IReadOnlyList<LoopRecord> GetLoops();
|
||||
IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null);
|
||||
UpdateManifest GetUpdateFrom(string? subsystem, string? fromVersion, string? filter);
|
||||
UpdateManifest CreateUpdate(string? fromVersion, string? toVersion, string? changes, string? shaHash, long? length, string? subsystem, string? filter, IDictionary<string, object?>? dependencies);
|
||||
UpdateManifest RemoveUpdate(string? updateId);
|
||||
IReadOnlyList<MediaRecord> ListMedia(IReadOnlyList<string>? loopIds = null, long? after = null, long? before = null);
|
||||
IReadOnlyList<MediaRecord> GetMedia(IReadOnlyList<string> paths);
|
||||
IReadOnlyList<MediaRecord> RemoveMedia(IReadOnlyList<string> paths);
|
||||
MediaRecord CreateMedia(string loopId, string path, string type, string reference, bool isEncrypted, IDictionary<string, object?>? meta);
|
||||
IReadOnlyList<BackupRecord> GetBackups();
|
||||
bool ShouldCreateSymmetricKey(string loopId);
|
||||
string GetOrCreateSymmetricKey(string loopId);
|
||||
KeyRequestRecord CreateKeyRequest(string loopId, string publicKey);
|
||||
KeyRequestRecord GetKeyRequest(string loopId, string? requestId, string? publicKey);
|
||||
IReadOnlyList<KeyRequestRecord> GetIncomingKeyRequests();
|
||||
IReadOnlyList<KeyRequestRecord> GetBinaryRequests();
|
||||
IReadOnlyList<object> GetHolidays();
|
||||
void UpdateRobot(DeviceRegistration registration);
|
||||
}
|
||||
|
||||
@@ -50,9 +50,19 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
var servicePrefix = envelope.ServicePrefix ?? string.Empty;
|
||||
var operation = envelope.Operation ?? string.Empty;
|
||||
|
||||
if (servicePrefix.StartsWith("Log_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleLog(operation, envelope));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleBackup(operation));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleAccount(operation));
|
||||
return Task.FromResult(HandleAccount(operation, envelope));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Notification_", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -65,6 +75,21 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
return Task.FromResult(HandleLoop(operation));
|
||||
}
|
||||
|
||||
if (servicePrefix.Equals("Media_20160725", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleMedia(operation, envelope));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Key_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleKey(operation, envelope));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Person_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandlePerson(operation));
|
||||
}
|
||||
|
||||
if (servicePrefix.StartsWith("Robot_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult(HandleRobot(operation, envelope));
|
||||
@@ -78,28 +103,78 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
return Task.FromResult(ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
ok = true,
|
||||
service = servicePrefix,
|
||||
operation
|
||||
host = envelope.HostName,
|
||||
target = $"{servicePrefix}.{operation}".Trim('.'),
|
||||
operation,
|
||||
note = "unknown target default response"
|
||||
}));
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleAccount(string operation)
|
||||
private ProtocolDispatchResult HandleAccount(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
var account = stateStore.GetAccount();
|
||||
var body = envelope.TryParseBody();
|
||||
|
||||
return operation switch
|
||||
if (operation.Equals("CreateHubToken", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
"CreateHubToken" => ProtocolDispatchResult.Ok(new
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
token = stateStore.IssueHubToken(),
|
||||
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
||||
}),
|
||||
"CreateAccessToken" => ProtocolDispatchResult.Ok(new
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("CreateAccessToken", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
token = $"access-{account.AccountId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
|
||||
}),
|
||||
"Get" => ProtocolDispatchResult.Ok(new[]
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("CheckEmail", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var email = ReadString(body, "email") ?? string.Empty;
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
exists = email.Equals(account.Email, StringComparison.OrdinalIgnoreCase)
|
||||
});
|
||||
}
|
||||
|
||||
if (operation is "Create" or "Login")
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = account.AccountId,
|
||||
email = ReadString(body, "email") ?? account.Email,
|
||||
firstName = ReadString(body, "firstName") ?? account.FirstName,
|
||||
lastName = ReadString(body, "lastName") ?? account.LastName,
|
||||
gender = "unknown",
|
||||
birthday = 631152000000L,
|
||||
phoneNumber = "+10000000000",
|
||||
photoUrl = string.Empty,
|
||||
isActive = true,
|
||||
messagingAllowed = true,
|
||||
accessKeyId = account.AccessKeyId,
|
||||
secretAccessKey = account.SecretAccessKey,
|
||||
roles = Array.Empty<object>(),
|
||||
facebookConnected = false,
|
||||
termsAccepted = true
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var ids = ReadStringArray(body, "ids");
|
||||
var matches = ids.Count == 0 || ids.Contains(account.AccountId, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
@@ -110,15 +185,89 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
accessKeyId = account.AccessKeyId,
|
||||
secretAccessKey = account.SecretAccessKey
|
||||
}
|
||||
}),
|
||||
_ => ProtocolDispatchResult.Ok(new
|
||||
});
|
||||
}
|
||||
|
||||
if (operation is "Update" or "ResetKeys" or "Remove" or "ActivateByCode" or "ResendActivationCode" or
|
||||
"ChangePassword" or "SendPasswordReset" or "PasswordResetByCode" or "UpdatePhoto" or "RemovePhoto" or
|
||||
"VerifyPhoneByCode" or "AcceptTerms" or "FacebookConnect" or "FacebookMobileConnect")
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = account.AccountId,
|
||||
email = account.Email,
|
||||
firstName = account.FirstName,
|
||||
lastName = account.LastName
|
||||
})
|
||||
};
|
||||
lastName = account.LastName,
|
||||
accessKeyId = account.AccessKeyId,
|
||||
secretAccessKey = account.SecretAccessKey
|
||||
});
|
||||
}
|
||||
|
||||
if (operation is "ChangeEmail" or "SendPhoneVerificationCode")
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = account.AccountId
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("GetAccountByAccessToken", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = account.AccountId,
|
||||
accessKeyId = account.AccessKeyId,
|
||||
secretAccessKey = account.SecretAccessKey,
|
||||
email = account.Email,
|
||||
friendlyId = stateStore.GetRobot().RobotId,
|
||||
payload = ReadObject(body, "payload")
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("Search", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var query = (ReadString(body, "query") ?? string.Empty).ToLowerInvariant();
|
||||
var haystack = $"{account.Email} {account.FirstName} {account.LastName} {account.AccountId}".ToLowerInvariant();
|
||||
|
||||
return ProtocolDispatchResult.Ok(query.Length > 0 && haystack.Contains(query)
|
||||
? new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = account.AccountId,
|
||||
email = account.Email,
|
||||
firstName = account.FirstName,
|
||||
lastName = account.LastName
|
||||
}
|
||||
}
|
||||
: Array.Empty<object>());
|
||||
}
|
||||
|
||||
if (operation.Equals("FacebookPrepareLogin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
url = "https://example.com/facebook-login",
|
||||
client_id = "fake-client-id",
|
||||
scope = "email",
|
||||
response_type = "token",
|
||||
state = $"fb-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
redirect_uri = "https://api.jibo.com/facebook/callback"
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("ConfirmEmailReset", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new { });
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = account.AccountId,
|
||||
email = account.Email,
|
||||
firstName = account.FirstName,
|
||||
lastName = account.LastName
|
||||
});
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleNotification(string operation, ProtocolEnvelope envelope)
|
||||
@@ -131,7 +280,11 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
var body = envelope.TryParseBody();
|
||||
var deviceId = envelope.DeviceId
|
||||
?? ReadString(body, "deviceId")
|
||||
?? ReadString(body, "serial_number")
|
||||
?? ReadString(body, "serialNumber")
|
||||
?? ReadString(body, "cpuid")
|
||||
?? ReadString(body, "cpuId")
|
||||
?? ReadString(body, "robotId")
|
||||
?? "unknown-device";
|
||||
|
||||
stateStore.GetOrCreateDevice(deviceId, envelope.FirmwareVersion, envelope.ApplicationVersion);
|
||||
@@ -149,24 +302,181 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
var robot = stateStore.GetRobot();
|
||||
var account = stateStore.GetAccount();
|
||||
|
||||
return ProtocolDispatchResult.Ok(new[]
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetLoops().Select(loop => new
|
||||
{
|
||||
new
|
||||
id = loop.LoopId,
|
||||
name = loop.Name,
|
||||
owner = loop.OwnerAccountId,
|
||||
robot = loop.RobotId,
|
||||
robotFriendlyId = loop.RobotFriendlyId,
|
||||
members = Array.Empty<object>(),
|
||||
isSuspended = loop.IsSuspended,
|
||||
created = loop.CreatedUtc.ToUnixTimeMilliseconds(),
|
||||
updated = loop.UpdatedUtc.ToUnixTimeMilliseconds()
|
||||
}).ToArray());
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleLog(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
return operation switch
|
||||
{
|
||||
"PutEventsAsync" => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = "openjibo-default-loop",
|
||||
name = "OpenJibo Default Loop",
|
||||
owner = account.AccountId,
|
||||
robot = robot.RobotId,
|
||||
robotFriendlyId = robot.DeviceId,
|
||||
members = Array.Empty<object>(),
|
||||
isSuspended = false,
|
||||
created = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
updated = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
}
|
||||
});
|
||||
contentEncoding = "gzip",
|
||||
uploadUrl = "https://api.jibo.com/upload/log-events"
|
||||
}),
|
||||
"PutEvents" => ProtocolDispatchResult.Ok(new { }),
|
||||
"PutBinaryAsync" => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
url = "https://api.jibo.com/log/binary/fake-id",
|
||||
uploadUrl = "https://api.jibo.com/upload/log-binary"
|
||||
}),
|
||||
"PutAsrBinary" => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
bucketName = "openjibo-test",
|
||||
key = "asr/fake-key",
|
||||
uploadUrl = "https://api.jibo.com/upload/asr-binary"
|
||||
}),
|
||||
"NewKinesisCredentials" => ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
credentials = new
|
||||
{
|
||||
AccessKeyId = "fake-access-key",
|
||||
Expiration = DateTimeOffset.UtcNow.AddHours(1).ToString("O"),
|
||||
SecretAccessKey = "fake-secret",
|
||||
SessionToken = "fake-session"
|
||||
},
|
||||
region = "us-east-1",
|
||||
streamName = "openjibo-log-stream"
|
||||
}),
|
||||
_ => ProtocolDispatchResult.Ok(new { })
|
||||
};
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleMedia(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
var body = envelope.TryParseBody();
|
||||
|
||||
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.ListMedia(
|
||||
ReadStringArray(body, "loopIds"),
|
||||
ReadLong(body, "after"),
|
||||
ReadLong(body, "before")).Select(MapMedia).ToArray());
|
||||
}
|
||||
|
||||
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetMedia(ReadStringArray(body, "paths")).Select(MapMedia).ToArray());
|
||||
}
|
||||
|
||||
if (operation.Equals("Remove", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.RemoveMedia(ReadStringArray(body, "paths")).Select(MapMedia).ToArray());
|
||||
}
|
||||
|
||||
if (operation.Equals("Create", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var loopId = ReadHeader(envelope, "x-loop-id") ?? ReadString(body, "loopId") ?? stateStore.GetLoops()[0].LoopId;
|
||||
var path = ReadHeader(envelope, "x-path") ?? ReadString(body, "path") ?? $"/media/{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
|
||||
var type = ReadHeader(envelope, "x-type") ?? ReadString(body, "type") ?? "unknown";
|
||||
var reference = ReadHeader(envelope, "x-reference") ?? ReadString(body, "reference") ?? string.Empty;
|
||||
var isEncrypted = ReadBooleanHeader(envelope, "x-encrypted") || ReadBool(body, "isEncrypted");
|
||||
var meta = ReadObject(body, "meta");
|
||||
|
||||
return ProtocolDispatchResult.Ok(MapMedia(stateStore.CreateMedia(loopId, path, type, reference, isEncrypted, meta)));
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandlePerson(string operation)
|
||||
{
|
||||
if (operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetHolidays());
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleBackup(string operation)
|
||||
{
|
||||
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetBackups());
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope)
|
||||
{
|
||||
var body = envelope.TryParseBody();
|
||||
var loopId = ReadString(body, "loopId") ?? ReadString(body, "id") ?? stateStore.GetLoops()[0].LoopId;
|
||||
|
||||
if (operation.Equals("ShouldCreate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
shouldCreate = stateStore.ShouldCreateSymmetricKey(loopId)
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("CreateSymmetricKey", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var symmetricKey = stateStore.GetOrCreateSymmetricKey(loopId);
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
loopId,
|
||||
key = symmetricKey,
|
||||
symmetricKey,
|
||||
created = true
|
||||
});
|
||||
}
|
||||
|
||||
if (operation is "CreateRequest" or "RequestSymmetricKey")
|
||||
{
|
||||
var record = stateStore.CreateKeyRequest(loopId, ReadString(body, "publicKey") ?? string.Empty);
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = record.RequestId,
|
||||
loopId = record.LoopId
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("GetRequest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetKeyRequest(loopId, ReadString(body, "id"), ReadString(body, "publicKey")));
|
||||
}
|
||||
|
||||
if (operation.Equals("ListIncomingRequests", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetIncomingKeyRequests());
|
||||
}
|
||||
|
||||
if (operation.Equals("ListBinaryRequests", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(stateStore.GetBinaryRequests());
|
||||
}
|
||||
|
||||
if (operation is "Share" or "ShareSymmetricKey" or "ShareBinary")
|
||||
{
|
||||
return ProtocolDispatchResult.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
if (operation.Equals("LoadSymmetricKey", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var symmetricKey = stateStore.GetOrCreateSymmetricKey(loopId);
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
loopId,
|
||||
key = symmetricKey,
|
||||
symmetricKey
|
||||
});
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(new { ok = true, operation });
|
||||
}
|
||||
|
||||
private ProtocolDispatchResult HandleRobot(string operation, ProtocolEnvelope envelope)
|
||||
@@ -186,16 +496,28 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
};
|
||||
|
||||
stateStore.UpdateRobot(updated);
|
||||
robot = updated;
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
result = "ok"
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.Equals("GetRobot", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var profile = stateStore.GetRobotProfile();
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = ReadString(envelope.TryParseBody(), "id") ?? profile.RobotId,
|
||||
payload = profile.Payload,
|
||||
calibrationPayload = profile.CalibrationPayload,
|
||||
updated = profile.UpdatedUtc.ToUnixTimeMilliseconds(),
|
||||
created = profile.CreatedUtc.ToUnixTimeMilliseconds()
|
||||
});
|
||||
}
|
||||
|
||||
return ProtocolDispatchResult.Ok(new
|
||||
{
|
||||
id = robot.RobotId,
|
||||
friendlyId = robot.DeviceId,
|
||||
name = robot.FriendlyName,
|
||||
firmwareVersion = robot.FirmwareVersion,
|
||||
applicationVersion = robot.ApplicationVersion
|
||||
result = "ok"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,8 +531,21 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
return operation switch
|
||||
{
|
||||
"ListUpdates" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter).Select(MapUpdate).ToArray()),
|
||||
"ListUpdatesFrom" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter).Select(MapUpdate).ToArray()),
|
||||
"ListUpdatesFrom" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter)
|
||||
.Where(update => fromVersion is null || update.FromVersion.Equals(fromVersion, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(MapUpdate)
|
||||
.ToArray()),
|
||||
"GetUpdateFrom" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.GetUpdateFrom(subsystem, fromVersion, filter))),
|
||||
"CreateUpdate" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.CreateUpdate(
|
||||
fromVersion,
|
||||
ReadString(body, "toVersion"),
|
||||
ReadString(body, "changes"),
|
||||
ReadString(body, "shaHash"),
|
||||
ReadLong(body, "length"),
|
||||
subsystem,
|
||||
filter,
|
||||
ReadObject(body, "dependencies")))),
|
||||
"RemoveUpdate" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.RemoveUpdate(ReadString(body, "id")))),
|
||||
_ => ProtocolDispatchResult.Ok(Array.Empty<object>())
|
||||
};
|
||||
}
|
||||
@@ -221,6 +556,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
{
|
||||
_id = update.UpdateId,
|
||||
created = update.CreatedUtc.ToUnixTimeMilliseconds(),
|
||||
accountId = "usr_openjibo_owner",
|
||||
fromVersion = update.FromVersion,
|
||||
toVersion = update.ToVersion,
|
||||
changes = update.Changes,
|
||||
@@ -228,18 +564,31 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
shaHash = update.ShaHash,
|
||||
length = update.Length,
|
||||
subsystem = update.Subsystem,
|
||||
filter = update.Filter
|
||||
filter = update.Filter,
|
||||
dependencies = new Dictionary<string, object?>()
|
||||
};
|
||||
}
|
||||
|
||||
private static object MapMedia(MediaRecord item)
|
||||
{
|
||||
return new
|
||||
{
|
||||
path = item.Path,
|
||||
created = item.CreatedUtc.ToUnixTimeMilliseconds(),
|
||||
type = item.MediaType,
|
||||
reference = item.Reference,
|
||||
accountId = item.AccountId,
|
||||
loopId = item.LoopId,
|
||||
url = item.Url,
|
||||
isEncrypted = item.IsEncrypted,
|
||||
isDeleted = item.IsDeleted,
|
||||
meta = item.Meta
|
||||
};
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement? element, string propertyName)
|
||||
{
|
||||
if (element is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!element.Value.TryGetProperty(propertyName, out var property))
|
||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -248,4 +597,88 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
? property.GetString()
|
||||
: property.ToString();
|
||||
}
|
||||
|
||||
private static long? ReadLong(JsonElement? element, string propertyName)
|
||||
{
|
||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out var number))
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
return long.TryParse(property.ToString(), out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private static bool ReadBool(JsonElement? element, string propertyName)
|
||||
{
|
||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return property.ValueKind switch
|
||||
{
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => bool.TryParse(property.ToString(), out var parsed) && parsed
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ReadStringArray(JsonElement? element, string propertyName)
|
||||
{
|
||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return property.EnumerateArray()
|
||||
.Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() ?? string.Empty : item.ToString())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IDictionary<string, object?>? ReadObject(JsonElement? element, string propertyName)
|
||||
{
|
||||
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (property.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var child in property.EnumerateObject())
|
||||
{
|
||||
result[child.Name] = child.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => child.Value.GetString(),
|
||||
JsonValueKind.Number when child.Value.TryGetInt64(out var longValue) => longValue,
|
||||
JsonValueKind.Number when child.Value.TryGetDouble(out var doubleValue) => doubleValue,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
_ => child.Value.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? ReadHeader(ProtocolEnvelope envelope, string headerName)
|
||||
{
|
||||
return envelope.Headers.TryGetValue(headerName, out var value) ? value : null;
|
||||
}
|
||||
|
||||
private static bool ReadBooleanHeader(ProtocolEnvelope envelope, string headerName)
|
||||
{
|
||||
return envelope.Headers.TryGetValue(headerName, out var value) &&
|
||||
bool.TryParse(value, out var parsed) &&
|
||||
parsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class BackupRecord
|
||||
{
|
||||
public string BackupId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string Name { get; init; } = "backup";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class KeyRequestRecord
|
||||
{
|
||||
public string RequestId { get; init; } = Guid.NewGuid().ToString("N");
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string PublicKey { get; init; } = string.Empty;
|
||||
public string EncryptedKey { get; init; } = string.Empty;
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class LoopRecord
|
||||
{
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string Name { get; init; } = "OpenJibo Default Loop";
|
||||
public string OwnerAccountId { get; init; } = "usr_openjibo_owner";
|
||||
public string RobotId { get; init; } = "my-robot-name";
|
||||
public string RobotFriendlyId { get; init; } = "my-robot-serial-number";
|
||||
public bool IsSuspended { get; init; }
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class MediaRecord
|
||||
{
|
||||
public string Path { get; init; } = string.Empty;
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string MediaType { get; init; } = "unknown";
|
||||
public string Reference { get; init; } = string.Empty;
|
||||
public string AccountId { get; init; } = "usr_openjibo_owner";
|
||||
public string LoopId { get; init; } = "openjibo-default-loop";
|
||||
public string Url { get; init; } = string.Empty;
|
||||
public bool IsEncrypted { get; init; }
|
||||
public bool IsDeleted { get; init; }
|
||||
public IDictionary<string, object?> Meta { get; init; } = new Dictionary<string, object?>();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class ProtocolFixture
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public ProtocolEnvelope Request { get; init; } = new();
|
||||
public int ExpectedStatusCode { get; init; } = 200;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Jibo.Cloud.Domain.Models;
|
||||
|
||||
public sealed class RobotProfile
|
||||
{
|
||||
public string RobotId { get; init; } = "my-robot-name";
|
||||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public IDictionary<string, object?> Payload { get; init; } = new Dictionary<string, object?>();
|
||||
public IDictionary<string, object?> CalibrationPayload { get; init; } = new Dictionary<string, object?>();
|
||||
public DateTimeOffset UpdatedUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -9,8 +9,14 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
private readonly AccountProfile _account = new();
|
||||
private readonly ConcurrentDictionary<string, DeviceRegistration> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, CloudSession> _sessionsByToken = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, string> _symmetricKeys = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, KeyRequestRecord> _keyRequests = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<UpdateManifest> _updates;
|
||||
private readonly List<MediaRecord> _media = [];
|
||||
private readonly List<BackupRecord> _backups = [];
|
||||
private readonly List<LoopRecord> _loops;
|
||||
private DeviceRegistration _robot;
|
||||
private RobotProfile _robotProfile;
|
||||
|
||||
public InMemoryCloudStateStore()
|
||||
{
|
||||
@@ -25,6 +31,26 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
};
|
||||
|
||||
_devices[_robot.DeviceId] = _robot;
|
||||
_robotProfile = new RobotProfile
|
||||
{
|
||||
RobotId = _robot.RobotId,
|
||||
Payload = new Dictionary<string, object?>
|
||||
{
|
||||
["SSID"] = "my-ssid",
|
||||
["connectedAt"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
["platform"] = "12.10.0",
|
||||
["serialNumber"] = _robot.DeviceId
|
||||
}
|
||||
};
|
||||
_loops =
|
||||
[
|
||||
new LoopRecord
|
||||
{
|
||||
OwnerAccountId = _account.AccountId,
|
||||
RobotId = _robot.RobotId,
|
||||
RobotFriendlyId = _robot.DeviceId
|
||||
}
|
||||
];
|
||||
|
||||
_updates =
|
||||
[
|
||||
@@ -45,6 +71,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
|
||||
public DeviceRegistration GetRobot() => _robot;
|
||||
|
||||
public RobotProfile GetRobotProfile() => _robotProfile;
|
||||
|
||||
public DeviceRegistration GetOrCreateDevice(string deviceId, string? firmwareVersion, string? applicationVersion)
|
||||
{
|
||||
return _devices.AddOrUpdate(
|
||||
@@ -121,6 +149,8 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
return _sessionsByToken.GetValueOrDefault(token);
|
||||
}
|
||||
|
||||
public IReadOnlyList<LoopRecord> GetLoops() => _loops.ToArray();
|
||||
|
||||
public IReadOnlyList<UpdateManifest> ListUpdates(string? subsystem = null, string? filter = null)
|
||||
{
|
||||
return _updates
|
||||
@@ -141,9 +171,184 @@ public sealed class InMemoryCloudStateStore : ICloudStateStore
|
||||
};
|
||||
}
|
||||
|
||||
public UpdateManifest CreateUpdate(string? fromVersion, string? toVersion, string? changes, string? shaHash, long? length, string? subsystem, string? filter, IDictionary<string, object?>? dependencies)
|
||||
{
|
||||
var update = new UpdateManifest
|
||||
{
|
||||
UpdateId = $"upd-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
FromVersion = fromVersion ?? "unknown",
|
||||
ToVersion = toVersion ?? fromVersion ?? "unknown",
|
||||
Changes = changes ?? string.Empty,
|
||||
Url = $"https://api.jibo.com/update/upd-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
ShaHash = shaHash ?? "fake-sha-hash",
|
||||
Length = length ?? 0,
|
||||
Subsystem = subsystem ?? "unknown",
|
||||
Filter = filter
|
||||
};
|
||||
|
||||
_updates.Add(update);
|
||||
return update;
|
||||
}
|
||||
|
||||
public UpdateManifest RemoveUpdate(string? updateId)
|
||||
{
|
||||
var existing = _updates.FirstOrDefault(update => update.UpdateId == updateId);
|
||||
if (existing is not null)
|
||||
{
|
||||
_updates.Remove(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
return new UpdateManifest
|
||||
{
|
||||
UpdateId = updateId ?? "unknown-update",
|
||||
Changes = "Update not found",
|
||||
Url = "https://api.jibo.com/update/missing",
|
||||
ShaHash = "missing",
|
||||
Subsystem = "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyList<MediaRecord> ListMedia(IReadOnlyList<string>? loopIds = null, long? after = null, long? before = null)
|
||||
{
|
||||
return _media
|
||||
.Where(item => loopIds is null || loopIds.Count == 0 || loopIds.Contains(item.LoopId))
|
||||
.Where(item => after is null || item.CreatedUtc.ToUnixTimeMilliseconds() > after)
|
||||
.Where(item => before is null || item.CreatedUtc.ToUnixTimeMilliseconds() < before)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyList<MediaRecord> GetMedia(IReadOnlyList<string> paths)
|
||||
{
|
||||
return _media.Where(item => paths.Contains(item.Path)).ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyList<MediaRecord> RemoveMedia(IReadOnlyList<string> paths)
|
||||
{
|
||||
var replacements = new List<MediaRecord>();
|
||||
for (var i = 0; i < _media.Count; i++)
|
||||
{
|
||||
if (!paths.Contains(_media[i].Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var updated = new MediaRecord
|
||||
{
|
||||
Path = _media[i].Path,
|
||||
CreatedUtc = _media[i].CreatedUtc,
|
||||
MediaType = _media[i].MediaType,
|
||||
Reference = _media[i].Reference,
|
||||
AccountId = _media[i].AccountId,
|
||||
LoopId = _media[i].LoopId,
|
||||
Url = _media[i].Url,
|
||||
IsEncrypted = _media[i].IsEncrypted,
|
||||
IsDeleted = true,
|
||||
Meta = _media[i].Meta
|
||||
};
|
||||
|
||||
_media[i] = updated;
|
||||
replacements.Add(updated);
|
||||
}
|
||||
|
||||
return replacements;
|
||||
}
|
||||
|
||||
public MediaRecord CreateMedia(string loopId, string path, string type, string reference, bool isEncrypted, IDictionary<string, object?>? meta)
|
||||
{
|
||||
var item = new MediaRecord
|
||||
{
|
||||
Path = path,
|
||||
MediaType = type,
|
||||
Reference = reference,
|
||||
AccountId = _account.AccountId,
|
||||
LoopId = loopId,
|
||||
Url = $"https://api.jibo.com/media/{Uri.EscapeDataString(path)}",
|
||||
IsEncrypted = isEncrypted,
|
||||
Meta = meta ?? new Dictionary<string, object?>()
|
||||
};
|
||||
|
||||
_media.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BackupRecord> GetBackups() => _backups.ToArray();
|
||||
|
||||
public bool ShouldCreateSymmetricKey(string loopId) => true;
|
||||
|
||||
public string GetOrCreateSymmetricKey(string loopId)
|
||||
{
|
||||
return _symmetricKeys.GetOrAdd(loopId, key => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes($"open-jibo-symmetric-key:{key}")));
|
||||
}
|
||||
|
||||
public KeyRequestRecord CreateKeyRequest(string loopId, string publicKey)
|
||||
{
|
||||
var record = new KeyRequestRecord
|
||||
{
|
||||
RequestId = $"req-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
LoopId = loopId,
|
||||
PublicKey = publicKey
|
||||
};
|
||||
|
||||
_keyRequests[record.RequestId] = record;
|
||||
return record;
|
||||
}
|
||||
|
||||
public KeyRequestRecord GetKeyRequest(string loopId, string? requestId, string? publicKey)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(requestId) && _keyRequests.TryGetValue(requestId, out var record))
|
||||
{
|
||||
return record;
|
||||
}
|
||||
|
||||
return new KeyRequestRecord
|
||||
{
|
||||
RequestId = requestId ?? "unknown-request",
|
||||
LoopId = loopId,
|
||||
PublicKey = publicKey ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyList<KeyRequestRecord> GetIncomingKeyRequests() => [];
|
||||
|
||||
public IReadOnlyList<KeyRequestRecord> GetBinaryRequests() => [];
|
||||
|
||||
public IReadOnlyList<object> GetHolidays()
|
||||
{
|
||||
return
|
||||
[
|
||||
new
|
||||
{
|
||||
id = "easter-1",
|
||||
eventId = (string?)null,
|
||||
name = "Easter",
|
||||
category = "holiday",
|
||||
subcategory = (string?)null,
|
||||
loopId = _loops[0].LoopId,
|
||||
memberId = (string?)null,
|
||||
isEnabled = true,
|
||||
date = "2026-04-05",
|
||||
endDate = (string?)null,
|
||||
created = DateTimeOffset.UtcNow.ToString("O")
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public void UpdateRobot(DeviceRegistration registration)
|
||||
{
|
||||
_robot = registration;
|
||||
_devices[registration.DeviceId] = registration;
|
||||
_robotProfile = new RobotProfile
|
||||
{
|
||||
RobotId = registration.RobotId,
|
||||
Payload = new Dictionary<string, object?>
|
||||
{
|
||||
["SSID"] = "my-ssid",
|
||||
["connectedAt"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
["platform"] = registration.FirmwareVersion ?? "12.10.0",
|
||||
["serialNumber"] = registration.DeviceId
|
||||
},
|
||||
UpdatedUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text.Json;
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
|
||||
namespace Jibo.Cloud.Tests.Fixtures;
|
||||
|
||||
internal static class ProtocolFixtureLoader
|
||||
{
|
||||
public static ProtocolFixture Load(string relativePath)
|
||||
{
|
||||
var fullPath = Path.Combine(AppContext.BaseDirectory, relativePath);
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(fullPath));
|
||||
var root = document.RootElement;
|
||||
|
||||
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (root.TryGetProperty("headers", out var headerElement) && headerElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in headerElement.EnumerateObject())
|
||||
{
|
||||
headers[property.Name] = property.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
var bodyText = root.TryGetProperty("body", out var bodyElement)
|
||||
? bodyElement.GetRawText()
|
||||
: string.Empty;
|
||||
|
||||
var target = headers.TryGetValue("x-amz-target", out var targetValue)
|
||||
? targetValue
|
||||
: string.Empty;
|
||||
var targetParts = target.Split('.', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new ProtocolFixture
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(relativePath),
|
||||
Request = new ProtocolEnvelope
|
||||
{
|
||||
HostName = root.TryGetProperty("host", out var hostElement) ? hostElement.GetString() ?? "api.jibo.com" : "api.jibo.com",
|
||||
Method = root.TryGetProperty("method", out var methodElement) ? methodElement.GetString() ?? "POST" : "POST",
|
||||
Path = root.TryGetProperty("path", out var pathElement) ? pathElement.GetString() ?? "/" : "/",
|
||||
Headers = headers,
|
||||
ServicePrefix = targetParts.Length > 0 ? targetParts[0] : null,
|
||||
Operation = targetParts.Length > 1 ? targetParts[1] : null,
|
||||
BodyText = bodyText
|
||||
},
|
||||
ExpectedStatusCode = 200
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,11 @@
|
||||
<ProjectReference Include="..\..\src\Jibo.Runtime.Abstractions\Jibo.Runtime.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\Jibo.Cloud\node\fixtures\http\*.json">
|
||||
<Link>fixtures\%(Filename)%(Extension)</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -61,4 +61,82 @@ public sealed class JiboCloudProtocolServiceTests
|
||||
Assert.Equal("robot", payload.RootElement.GetProperty("subsystem").GetString());
|
||||
Assert.True(payload.RootElement.TryGetProperty("url", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutEventsAsync_ReturnsUploadUrl()
|
||||
{
|
||||
var result = await _service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Log_20160715",
|
||||
Operation = "PutEventsAsync",
|
||||
BodyText = "{}"
|
||||
});
|
||||
|
||||
using var payload = JsonDocument.Parse(result.BodyText);
|
||||
Assert.Equal("gzip", payload.RootElement.GetProperty("contentEncoding").GetString());
|
||||
Assert.Contains("/upload/log-events", payload.RootElement.GetProperty("uploadUrl").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MediaCreateAndGet_ReturnsCreatedItem()
|
||||
{
|
||||
var created = await _service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Media_20160725",
|
||||
Operation = "Create",
|
||||
BodyText = """{"path":"/media/test-item","type":"image","reference":"demo"}"""
|
||||
});
|
||||
|
||||
using var createdPayload = JsonDocument.Parse(created.BodyText);
|
||||
Assert.Equal("/media/test-item", createdPayload.RootElement.GetProperty("path").GetString());
|
||||
|
||||
var fetched = await _service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Media_20160725",
|
||||
Operation = "Get",
|
||||
BodyText = """{"paths":["/media/test-item"]}"""
|
||||
});
|
||||
|
||||
using var fetchedPayload = JsonDocument.Parse(fetched.BodyText);
|
||||
Assert.Single(fetchedPayload.RootElement.EnumerateArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KeyCreateSymmetricKey_ReturnsKeyPayload()
|
||||
{
|
||||
var result = await _service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Key_20160715",
|
||||
Operation = "CreateSymmetricKey",
|
||||
BodyText = """{"loopId":"openjibo-default-loop"}"""
|
||||
});
|
||||
|
||||
using var payload = JsonDocument.Parse(result.BodyText);
|
||||
Assert.Equal("openjibo-default-loop", payload.RootElement.GetProperty("loopId").GetString());
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.RootElement.GetProperty("key").GetString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PersonListHolidays_ReturnsHoliday()
|
||||
{
|
||||
var result = await _service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
ServicePrefix = "Person_20160715",
|
||||
Operation = "ListHolidays",
|
||||
BodyText = "{}"
|
||||
});
|
||||
|
||||
using var payload = JsonDocument.Parse(result.BodyText);
|
||||
Assert.Single(payload.RootElement.EnumerateArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Jibo.Cloud.Application.Services;
|
||||
using Jibo.Cloud.Infrastructure.Persistence;
|
||||
using Jibo.Cloud.Tests.Fixtures;
|
||||
|
||||
namespace Jibo.Cloud.Tests.Protocol;
|
||||
|
||||
public sealed class ProtocolFixtureReplayTests
|
||||
{
|
||||
private readonly JiboCloudProtocolService _service = new(new InMemoryCloudStateStore());
|
||||
|
||||
[Theory]
|
||||
[InlineData("fixtures\\create-hub-token.request.json")]
|
||||
[InlineData("fixtures\\new-robot-token.request.json")]
|
||||
public async Task FixtureRequest_ReplaysSuccessfully(string relativePath)
|
||||
{
|
||||
var fixture = ProtocolFixtureLoader.Load(relativePath);
|
||||
var result = await _service.DispatchAsync(fixture.Request);
|
||||
|
||||
Assert.Equal(fixture.ExpectedStatusCode, result.StatusCode);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.BodyText));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user