diff --git a/src/aws.zig b/src/aws.zig index 9cb804e..8d0fe40 100644 --- a/src/aws.zig +++ b/src/aws.zig @@ -1,21 +1,19 @@ const builtin = @import("builtin"); -const case = @import("case"); const std = @import("std"); + +const case = @import("case"); +const date = @import("date"); +const json = @import("json"); const zeit = @import("zeit"); const awshttp = @import("aws_http.zig"); -const json = @import("json"); const url = @import("url.zig"); -const date = @import("date"); const servicemodel = @import("servicemodel.zig"); const xml_shaper = @import("xml_shaper.zig"); const xml_serializer = @import("xml_serializer.zig"); const scoped_log = std.log.scoped(.aws); -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; - /// control all logs directly/indirectly used by aws sdk. Not recommended for /// use under normal circumstances, but helpful for times when the zig logging /// controls are insufficient (e.g. use in build script) @@ -95,7 +93,7 @@ pub const Options = struct { pub const Diagnostics = struct { http_code: i64, response_body: []const u8, - allocator: Allocator, + allocator: std.mem.Allocator, pub fn deinit(self: *Diagnostics) void { self.allocator.free(self.response_body); @@ -117,12 +115,12 @@ pub const ClientOptions = struct { proxy: ?std.http.Client.Proxy = null, }; pub const Client = struct { - allocator: Allocator, + allocator: std.mem.Allocator, aws_http: awshttp.AwsHttp, const Self = @This(); - pub fn init(allocator: Allocator, options: ClientOptions) Self { + pub fn init(allocator: std.mem.Allocator, options: ClientOptions) Self { return Self{ .allocator = allocator, .aws_http = awshttp.AwsHttp.init(allocator, options.proxy), @@ -470,7 +468,7 @@ pub fn Request(comptime request_action: anytype) type { } fn setHeaderValue( - allocator: Allocator, + allocator: std.mem.Allocator, response: anytype, comptime field_name: []const u8, comptime field_type: type, @@ -500,7 +498,7 @@ pub fn Request(comptime request_action: anytype) type { var buf_request_id: [256]u8 = undefined; const request_id = try requestIdFromHeaders(&buf_request_id, options.client.allocator, aws_request, response); - const arena = ArenaAllocator.init(options.client.allocator); + const arena = std.heap.ArenaAllocator.init(options.client.allocator); if (@hasDecl(action.Response, "http_payload")) { var rc = try FullResponseType.init(.{ @@ -556,7 +554,7 @@ pub fn Request(comptime request_action: anytype) type { } fn jsonReturn(aws_request: awshttp.HttpRequest, options: Options, response: awshttp.HttpResult) !FullResponseType { - var arena = ArenaAllocator.init(options.client.allocator); + var arena = std.heap.ArenaAllocator.init(options.client.allocator); const parser_options = json.ParseOptions{ .allocator = arena.allocator(), @@ -664,7 +662,7 @@ pub fn Request(comptime request_action: anytype) type { // } // // Big thing is that requestid, which we'll need to fetch "manually" - var arena = ArenaAllocator.init(options.client.allocator); + var arena = std.heap.ArenaAllocator.init(options.client.allocator); const xml_options = xml_shaper.ParseOptions{ .allocator = arena.allocator(), @@ -769,7 +767,7 @@ pub fn Request(comptime request_action: anytype) type { fn ParsedJsonData(comptime T: type) type { return struct { parsed_response_ptr: *T, - allocator: Allocator, + allocator: std.mem.Allocator, const MySelf = @This(); @@ -890,7 +888,7 @@ fn parseInt(comptime T: type, val: []const u8) !T { return rc; } -fn generalAllocPrint(allocator: Allocator, val: anytype) !?[]const u8 { +fn generalAllocPrint(allocator: std.mem.Allocator, val: anytype) !?[]const u8 { const T = @TypeOf(val); switch (@typeInfo(T)) { .optional => if (val) |v| return generalAllocPrint(allocator, v) else return null, @@ -913,7 +911,7 @@ fn generalAllocPrint(allocator: Allocator, val: anytype) !?[]const u8 { else => return try std.fmt.allocPrint(allocator, "{any}", .{val}), } } -fn headersFor(allocator: Allocator, request: anytype) ![]awshttp.Header { +fn headersFor(allocator: std.mem.Allocator, request: anytype) ![]awshttp.Header { log.debug("Checking for headers to include for type {}", .{@TypeOf(request)}); if (!@hasDecl(@TypeOf(request), "http_header")) return &[_]awshttp.Header{}; const http_header = @TypeOf(request).http_header; @@ -938,7 +936,7 @@ fn headersFor(allocator: Allocator, request: anytype) ![]awshttp.Header { return headers.toOwnedSlice(allocator); } -fn freeHeadersFor(allocator: Allocator, request: anytype, headers: []const awshttp.Header) void { +fn freeHeadersFor(allocator: std.mem.Allocator, request: anytype, headers: []const awshttp.Header) void { if (!@hasDecl(@TypeOf(request), "http_header")) return; const http_header = @TypeOf(request).http_header; const fields = std.meta.fields(@TypeOf(http_header)); @@ -999,7 +997,7 @@ fn getContentType(headers: []const awshttp.Header) !ContentType { } /// Get request ID from headers. /// Allocation is only used in case of an error. Caller does not need to free the returned buffer. -fn requestIdFromHeaders(buf: []u8, allocator: Allocator, request: awshttp.HttpRequest, response: awshttp.HttpResult) ![]u8 { +fn requestIdFromHeaders(buf: []u8, allocator: std.mem.Allocator, request: awshttp.HttpRequest, response: awshttp.HttpResult) ![]u8 { var rid: ?[]const u8 = null; // This "thing" is called: // * Host ID @@ -1093,13 +1091,13 @@ fn FullResponse(comptime action: anytype) type { response: action.Response = undefined, request_id: []const u8, raw_parsed: RawParsed = .{ .raw = undefined }, - arena: ArenaAllocator, + arena: std.heap.ArenaAllocator, }; response: action.Response = undefined, raw_parsed: RawParsed = .{ .raw = undefined }, response_metadata: ResponseMetadata, - arena: ArenaAllocator, + arena: std.heap.ArenaAllocator, const Self = @This(); @@ -1122,14 +1120,14 @@ fn FullResponse(comptime action: anytype) type { } }; } -fn safeFree(allocator: Allocator, obj: anytype) void { +fn safeFree(allocator: std.mem.Allocator, obj: anytype) void { switch (@typeInfo(@TypeOf(obj))) { .pointer => allocator.free(obj), .optional => if (obj) |o| safeFree(allocator, o), else => {}, } } -fn queryFieldTransformer(allocator: Allocator, field_name: []const u8) anyerror![]const u8 { +fn queryFieldTransformer(allocator: std.mem.Allocator, field_name: []const u8) anyerror![]const u8 { var reader = std.Io.Reader.fixed(field_name); var aw = try std.Io.Writer.Allocating.initCapacity(allocator, 100); defer aw.deinit(); @@ -1140,7 +1138,7 @@ fn queryFieldTransformer(allocator: Allocator, field_name: []const u8) anyerror! } fn buildPath( - allocator: Allocator, + allocator: std.mem.Allocator, raw_uri: []const u8, comptime ActionRequest: type, request: anytype, @@ -1228,7 +1226,7 @@ fn uriEncodeByte(char: u8, writer: *std.Io.Writer, encode_slash: bool) !void { } } -fn buildQuery(allocator: Allocator, request: anytype) ![]const u8 { +fn buildQuery(allocator: std.mem.Allocator, request: anytype) ![]const u8 { // query should look something like this: // pub const http_query = .{ // .master_region = "MasterRegion", @@ -1358,7 +1356,7 @@ const IgnoringWriter = struct { }; fn reportTraffic( - allocator: Allocator, + allocator: std.mem.Allocator, info: []const u8, request: awshttp.HttpRequest, response: awshttp.HttpResult, @@ -1391,1399 +1389,9 @@ fn reportTraffic( reporter("{s}\n", .{msg.written()}); } -//////////////////////////////////////////////////////////////////////// -// All code below this line is for testing -//////////////////////////////////////////////////////////////////////// - -// TODO: Where does this belong really? -fn typeForField(comptime T: type, comptime field_name: []const u8) !type { - const ti = @typeInfo(T); - switch (ti) { - .@"struct" => { - inline for (ti.@"struct".fields) |field| { - if (std.mem.eql(u8, field.name, field_name)) - return field.type; - } - }, - else => return error.TypeIsNotAStruct, // should not hit this - } - return error.FieldNotFound; -} - -test "custom serialization for map objects" { - const allocator = std.testing.allocator; - var buffer = std.Io.Writer.Allocating.init(allocator); - defer buffer.deinit(); - var tags = try std.ArrayList(@typeInfo(try typeForField(services.lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 2); - defer tags.deinit(allocator); - tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" }); - tags.appendAssumeCapacity(.{ .key = "Baz", .value = "Qux" }); - const req = services.lambda.TagResourceRequest{ .resource = "hello", .tags = tags.items }; - try buffer.writer.print("{f}", .{std.json.fmt(req, .{ .whitespace = .indent_4 })}); - - const parsed_body = try std.json.parseFromSlice(struct { - Resource: []const u8, - Tags: struct { - Foo: []const u8, - Baz: []const u8, - }, - }, testing.allocator, buffer.written(), .{}); - defer parsed_body.deinit(); - - try testing.expectEqualStrings("hello", parsed_body.value.Resource); - try testing.expectEqualStrings("Bar", parsed_body.value.Tags.Foo); - try testing.expectEqualStrings("Qux", parsed_body.value.Tags.Baz); -} - -test "proper serialization for kms" { - // Github issue #8 - // https://github.com/elerch/aws-sdk-for-zig/issues/8 - const allocator = std.testing.allocator; - var buffer = std.Io.Writer.Allocating.init(allocator); - defer buffer.deinit(); - const req = services.kms.encrypt.Request{ - .encryption_algorithm = "SYMMETRIC_DEFAULT", - // Since encryption_context is not null, we expect "{}" to be the value - // here, not "[]", because this is our special AWS map pattern - .encryption_context = &.{}, - .key_id = "42", - .plaintext = "foo", - .dry_run = false, - .grant_tokens = &[_][]const u8{}, - }; - try buffer.writer.print("{f}", .{std.json.fmt(req, .{ .whitespace = .indent_4 })}); - - { - const parsed_body = try std.json.parseFromSlice(struct { - KeyId: []const u8, - Plaintext: []const u8, - EncryptionContext: struct {}, - GrantTokens: [][]const u8, - EncryptionAlgorithm: []const u8, - DryRun: bool, - }, testing.allocator, buffer.written(), .{}); - defer parsed_body.deinit(); - - try testing.expectEqualStrings("42", parsed_body.value.KeyId); - try testing.expectEqualStrings("foo", parsed_body.value.Plaintext); - try testing.expectEqual(0, parsed_body.value.GrantTokens.len); - try testing.expectEqualStrings("SYMMETRIC_DEFAULT", parsed_body.value.EncryptionAlgorithm); - try testing.expectEqual(false, parsed_body.value.DryRun); - } - - var buffer_null = std.Io.Writer.Allocating.init(allocator); - defer buffer_null.deinit(); - const req_null = services.kms.encrypt.Request{ - .encryption_algorithm = "SYMMETRIC_DEFAULT", - // Since encryption_context here *IS* null, we expect simply "null" to be the value - .encryption_context = null, - .key_id = "42", - .plaintext = "foo", - .dry_run = false, - .grant_tokens = &[_][]const u8{}, - }; - - try buffer_null.writer.print("{f}", .{std.json.fmt(req_null, .{ .whitespace = .indent_4 })}); - - { - const parsed_body = try std.json.parseFromSlice(struct { - KeyId: []const u8, - Plaintext: []const u8, - EncryptionContext: ?struct {}, - GrantTokens: [][]const u8, - EncryptionAlgorithm: []const u8, - DryRun: bool, - }, testing.allocator, buffer_null.written(), .{}); - defer parsed_body.deinit(); - - try testing.expectEqualStrings("42", parsed_body.value.KeyId); - try testing.expectEqualStrings("foo", parsed_body.value.Plaintext); - try testing.expectEqual(null, parsed_body.value.EncryptionContext); - try testing.expectEqual(0, parsed_body.value.GrantTokens.len); - try testing.expectEqualStrings("SYMMETRIC_DEFAULT", parsed_body.value.EncryptionAlgorithm); - try testing.expectEqual(false, parsed_body.value.DryRun); - } -} - -test "REST Json v1 builds proper queries" { - const allocator = std.testing.allocator; - const svs = Services(.{.lambda}){}; - const request = svs.lambda.list_functions.Request{ - .max_items = 1, - }; - const query = try buildQuery(allocator, request); - defer allocator.free(query); - try std.testing.expectEqualStrings("?MaxItems=1", query); -} -test "REST Json v1 handles reserved chars in queries" { - const allocator = std.testing.allocator; - const svs = Services(.{.lambda}){}; - var keys = [_][]const u8{"Foo?I'm a crazy%dude"}; // Would love to have a way to express this without burning a var here - const request = svs.lambda.untag_resource.Request{ - .tag_keys = keys[0..], - .resource = "hello", - }; - const query = try buildQuery(allocator, request); - defer allocator.free(query); - try std.testing.expectEqualStrings("?tagKeys=Foo%3FI%27m a crazy%25dude", query); -} -test "REST Json v1 serializes lists in queries" { - const allocator = std.testing.allocator; - const svs = Services(.{.lambda}){}; - var keys = [_][]const u8{ "Foo", "Bar" }; // Would love to have a way to express this without burning a var here - const request = svs.lambda.untag_resource.Request{ - .tag_keys = keys[0..], - .resource = "hello", - }; - const query = try buildQuery(allocator, request); - defer allocator.free(query); - try std.testing.expectEqualStrings("?tagKeys=Foo&tagKeys=Bar", query); -} -test "REST Json v1 buildpath substitutes" { - const allocator = std.testing.allocator; - var al = std.ArrayList([]const u8){}; - defer al.deinit(allocator); - const svs = Services(.{.lambda}){}; - const request = svs.lambda.list_functions.Request{ - .max_items = 1, - }; - const input_path = "https://myhost/{MaxItems}/"; - const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true, &al); - defer allocator.free(output_path); - try std.testing.expectEqualStrings("https://myhost/1/", output_path); -} -test "REST Json v1 buildpath handles restricted characters" { - const allocator = std.testing.allocator; - var al = std.ArrayList([]const u8){}; - defer al.deinit(allocator); - const svs = Services(.{.lambda}){}; - const request = svs.lambda.list_functions.Request{ - .marker = ":", - }; - const input_path = "https://myhost/{Marker}/"; - const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true, &al); - defer allocator.free(output_path); - try std.testing.expectEqualStrings("https://myhost/%3A/", output_path); -} -test "basic json request serialization" { - const allocator = std.testing.allocator; - const svs = Services(.{.dynamo_db}){}; - const request = svs.dynamo_db.list_tables.Request{ - .limit = 1, - }; - var buffer = std.Io.Writer.Allocating.init(allocator); - defer buffer.deinit(); - - // The transformer needs to allocate stuff out of band, but we - // can guarantee we don't need the memory after this call completes, - // so we'll use an arena allocator to whack everything. - // TODO: Determine if sending in null values is ok, or if we need another - // tweak to the stringify function to exclude. According to the - // smithy spec, "A null value MAY be provided or omitted - // for a boxed member with no observable difference." But we're - // seeing a lot of differences here between spec and reality - // - try buffer.writer.print("{f}", .{std.json.fmt(request, .{ .whitespace = .indent_4 })}); - try std.testing.expectEqualStrings( - \\{ - \\ "ExclusiveStartTableName": null, - \\ "Limit": 1 - \\} - , buffer.written()); -} -test "layer object only" { - const TestResponse = struct { - arn: ?[]const u8 = null, - // uncompressed_code_size: ?i64 = null, - - pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 { - const mappings = .{ - .arn = "Arn", - }; - return @field(mappings, field_name); - } - }; - const response = - \\ { - \\ "UncompressedCodeSize": 2, - \\ "Arn": "blah" - \\ } - ; - // const response = - // \\ { - // \\ "UncompressedCodeSize": 22599541, - // \\ "Arn": "arn:aws:lambda:us-west-2:123456789012:layer:PollyNotes-lib:4" - // \\ } - // ; - const allocator = std.testing.allocator; - var stream = json.TokenStream.init(response); - const parser_options = json.ParseOptions{ - .allocator = allocator, - .allow_camel_case_conversion = true, // new option - .allow_snake_case_conversion = true, // new option - .allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though - .allow_missing_fields = false, // new option. Cannot yet handle non-struct fields though - }; - const r = try json.parse(TestResponse, &stream, parser_options); - json.parseFree(TestResponse, r, parser_options); -} - -// Use for debugging json responses of specific requests -// test "dummy request" { -// const allocator = std.testing.allocator; -// const svs = Services(.{.sts}){}; -// const request = svs.sts.get_session_token.Request{ -// .duration_seconds = 900, -// }; -// const FullR = FullResponse(request); -// const response = -// var stream = json.TokenStream.init(response); -// -// const parser_options = json.ParseOptions{ -// .allocator = allocator, -// .allow_camel_case_conversion = true, // new option -// .allow_snake_case_conversion = true, // new option -// .allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though -// .allow_missing_fields = false, // new option. Cannot yet handle non-struct fields though -// }; -// const SResponse = ServerResponse(request); -// const r = try json.parse(SResponse, &stream, parser_options); -// json.parseFree(SResponse, r, parser_options); - test { - // To run nested container tests, either, call `refAllDecls` which will - // reference all declarations located in the given argument. - // `@This()` is a builtin function that returns the innermost container it is called from. - // In this example, the innermost container is this file (implicitly a struct). - std.testing.refAllDecls(@This()); - std.testing.refAllDecls(awshttp); - std.testing.refAllDecls(json); - std.testing.refAllDecls(url); - std.testing.refAllDecls(case); - std.testing.refAllDecls(date); - std.testing.refAllDecls(servicemodel); - std.testing.refAllDecls(xml_shaper); -} -const TestOptions = struct { - allocator: Allocator, - arena: ?*ArenaAllocator = null, - server_port: ?u16 = null, - server_remaining_requests: usize = 1, - server_response: []const u8 = "unset", - server_response_status: std.http.Status = .ok, - server_response_headers: []const std.http.Header = &.{}, - server_response_transfer_encoding: ?std.http.TransferEncoding = null, - request_body: []u8 = "", - request_method: std.http.Method = undefined, - request_target: []const u8 = undefined, - request_headers: []std.http.Header = undefined, - test_server_runtime_uri: ?[]u8 = null, - server_ready: std.Thread.Semaphore = .{}, - requests_processed: usize = 0, - - const Self = @This(); - - /// Builtin hashmap for strings as keys. - /// Key memory is managed by the caller. Keys and values - /// will not automatically be freed. - pub fn StringCaseInsensitiveHashMap(comptime V: type) type { - return std.HashMap([]const u8, V, StringInsensitiveContext, std.hash_map.default_max_load_percentage); - } - - pub const StringInsensitiveContext = struct { - pub fn hash(self: @This(), s: []const u8) u64 { - _ = self; - return hashString(s); - } - pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { - _ = self; - return eqlString(a, b); - } + _ = @import("aws_test.zig"){ + .buildQuery = buildQuery, + .buildPath = buildPath, }; - - pub fn eqlString(a: []const u8, b: []const u8) bool { - return std.ascii.eqlIgnoreCase(a, b); - } - - pub fn hashString(s: []const u8) u64 { - var buf: [1024]u8 = undefined; - if (s.len > buf.len) unreachable; // tolower has a debug assert, but we want non-debug check too - const lower_s = std.ascii.lowerString(buf[0..], s); - return std.hash.Wyhash.hash(0, lower_s); - } - - fn expectNoDuplicateHeaders(self: *Self) !void { - // As header keys are - var hm = StringCaseInsensitiveHashMap(void).init(self.allocator); - try hm.ensureTotalCapacity(@intCast(self.request_headers.len)); - defer hm.deinit(); - for (self.request_headers) |h| { - if (hm.getKey(h.name)) |_| { - log.err("Duplicate key detected. Key name: {s}", .{h.name}); - return error.duplicateKeyDetected; - } - try hm.put(h.name, {}); - } - } - - fn expectHeader(self: *Self, name: []const u8, value: []const u8) !void { - for (self.request_headers) |h| - if (std.ascii.eqlIgnoreCase(name, h.name) and - std.mem.eql(u8, value, h.value)) return; - return error.HeaderOrValueNotFound; - } - fn waitForReady(self: *Self) !void { - // Set 10s timeout...this is way longer than necessary - log.debug("waiting for ready", .{}); - try self.server_ready.timedWait(1000 * std.time.ns_per_ms); - // var deadline = std.Thread.Futex.Deadline.init(1000 * std.time.ns_per_ms); - // if (self.futex_word.load(.acquire) != 0) return; - // log.debug("futex zero", .{}); - // // note that this seems backwards from the documentation... - // deadline.wait(self.futex_word, 1) catch { - // log.err("futex value {d}", .{self.futex_word.load(.acquire)}); - // return error.TestServerTimeoutWaitingForReady; - // }; - log.debug("the wait is over!", .{}); - } -}; - -/// This starts a test server. We're not testing the server itself, -/// so the main tests will start this thing up and create an arena around the -/// whole thing so we can just deallocate everything at once at the end, -/// leaks be damned -fn threadMain(options: *TestOptions) !void { - // https://github.com/ziglang/zig/blob/d2be725e4b14c33dbd39054e33d926913eee3cd4/lib/compiler/std-docs.zig#L22-L54 - - options.arena = try options.allocator.create(ArenaAllocator); - options.arena.?.* = ArenaAllocator.init(options.allocator); - const allocator = options.arena.?.allocator(); - options.allocator = allocator; - - const address = try std.net.Address.parseIp("127.0.0.1", 0); - var http_server = try address.listen(.{}); - options.server_port = http_server.listen_address.in.getPort(); - // TODO: remove - options.test_server_runtime_uri = try std.fmt.allocPrint(options.allocator, "http://127.0.0.1:{d}", .{options.server_port.?}); - log.debug("server listening at {s}", .{options.test_server_runtime_uri.?}); - log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()}); - // var arena = ArenaAllocator.init(options.allocator); - // defer arena.deinit(); - // var aa = arena.allocator(); - // We're in control of all requests/responses, so this flag will tell us - // when it's time to shut down - if (options.server_remaining_requests == 0) - options.server_ready.post(); // This will cause the wait for server to return - while (options.server_remaining_requests > 0) : (options.server_remaining_requests -= 1) { - processRequest(options, &http_server) catch |e| { - log.err("Unexpected error processing request: {any}", .{e}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - }; - } -} - -fn processRequest(options: *TestOptions, net_server: *std.net.Server) !void { - log.debug( - "tid {d} (server): server waiting to accept. requests remaining: {d}", - .{ std.Thread.getCurrentId(), options.server_remaining_requests }, - ); - // options.futex_word.store(1, .release); - // errdefer options.futex_word.store(0, .release); - options.server_ready.post(); - var connection = try net_server.accept(); - defer connection.stream.close(); - var read_buffer: [1024 * 16]u8 = undefined; - var http_server = std.http.Server.init(connection, &read_buffer); - while (http_server.state == .ready) { - var request = http_server.receiveHead() catch |err| switch (err) { - error.HttpConnectionClosing => return, - else => { - std.log.err("closing http connection: {s}", .{@errorName(err)}); - std.log.debug("Error occurred from this request: \n{s}", .{read_buffer[0..http_server.read_buffer_len]}); - return; - }, - }; - try serveRequest(options, &request); - } -} - -fn serveRequest(options: *TestOptions, request: *std.http.Server.Request) !void { - options.requests_processed += 1; - options.request_body = try (try request.reader()).readAllAlloc(options.allocator, std.math.maxInt(usize)); - options.request_method = request.head.method; - options.request_target = try options.allocator.dupe(u8, request.head.target); - var req_headers = std.ArrayList(std.http.Header).init(options.allocator); - defer req_headers.deinit(); - var it = request.iterateHeaders(); - while (it.next()) |f| { - const h = try options.allocator.create(std.http.Header); - h.* = .{ .name = try options.allocator.dupe(u8, f.name), .value = try options.allocator.dupe(u8, f.value) }; - try req_headers.append(h.*); - } - options.request_headers = try req_headers.toOwnedSlice(); - log.debug( - "tid {d} (server): {d} bytes read from request", - .{ std.Thread.getCurrentId(), options.request_body.len }, - ); - - // try response.headers.append("content-type", "text/plain"); - try request.respond(options.server_response, .{ - .status = options.server_response_status, - .extra_headers = options.server_response_headers, - }); - - log.debug( - "tid {d} (server): sent response", - .{std.Thread.getCurrentId()}, - ); -} - -//////////////////////////////////////////////////////////////////////// -// These will replicate the tests that were in src/main.zig -// The server_response and server_response_headers come from logs of -// a previous run of src/main.zig, with redactions -//////////////////////////////////////////////////////////////////////// - -const TestSetup = struct { - allocator: Allocator, - request_options: TestOptions, - server_thread: std.Thread = undefined, - creds: aws_auth.Credentials = undefined, - client: Client = undefined, - started: bool = false, - - const Self = @This(); - - const aws_creds = @import("aws_credentials.zig"); - const aws_auth = @import("aws_authentication.zig"); - const signing_time = - date.dateTimeToTimestamp(date.parseIso8601ToDateTime("20230908T170252Z") catch @compileError("Cannot parse date")) catch @compileError("Cannot parse date"); - - fn init(options: TestOptions) Self { - return .{ - .request_options = options, - .allocator = options.allocator, - }; - } - - fn start(self: *Self) !Options { - self.server_thread = try std.Thread.spawn( - .{}, - threadMain, - .{&self.request_options}, - ); - self.started = true; - try self.request_options.waitForReady(); - // Not sure why we're getting sprayed here, but we have an arena allocator, and this - // is testing, so yolo - awshttp.endpoint_override = self.request_options.test_server_runtime_uri; - if (awshttp.endpoint_override == null) return error.TestSetupStartFailure; - std.log.debug("endpoint override set to {?s}", .{awshttp.endpoint_override}); - self.creds = aws_auth.Credentials.init( - self.allocator, - try self.allocator.dupe(u8, "ACCESS"), - try self.allocator.dupe(u8, "SECRET"), - null, - ); - aws_creds.static_credentials = self.creds; - const client = Client.init(self.allocator, .{}); - self.client = client; - return .{ - .region = "us-west-2", - .client = client, - .signing_time = signing_time, - }; - } - - fn stop(self: *Self) void { - if (self.request_options.server_remaining_requests > 0) - if (test_error_log_enabled) - std.log.err( - "Test server has {d} request(s) remaining to issue! Draining", - .{self.request_options.server_remaining_requests}, - ) - else - std.log.info( - "Test server has {d} request(s) remaining to issue! Draining", - .{self.request_options.server_remaining_requests}, - ); - - var rr = self.request_options.server_remaining_requests; - while (rr > 0) : (rr -= 1) { - std.log.debug("rr: {d}", .{self.request_options.server_remaining_requests}); - // We need to drain all remaining requests, otherwise the server - // will hang indefinitely - var client = std.http.Client{ .allocator = self.allocator }; - defer client.deinit(); - _ = client.fetch(.{ .location = .{ .url = self.request_options.test_server_runtime_uri.? } }) catch unreachable; - } - self.server_thread.join(); - } - - fn deinit(self: *Self) void { - if (self.request_options.arena) |a| { - a.deinit(); - self.allocator.destroy(a); - } - if (!self.started) return; - awshttp.endpoint_override = null; - // creds.deinit(); Creds will get deinited in the course of the call. We don't want to do it twice - aws_creds.static_credentials = null; // we do need to reset the static creds for the next user though - self.client.deinit(); - } -}; - -test "query_no_input: sts getCallerIdentity comptime" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"GetCallerIdentityResponse":{"GetCallerIdentityResult":{"Account":"123456789012","Arn":"arn:aws:iam::123456789012:user/admin","UserId":"AIDAYAM4POHXHRVANDQBQ"},"ResponseMetadata":{"RequestId":"8f0d54da-1230-40f7-b4ac-95015c4b84cd"}}} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "8f0d54da-1230-40f7-b4ac-95015c4b84cd" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const sts = (Services(.{.sts}){}).sts; - const call = try Request(sts.get_caller_identity).call(.{}, options); - // const call = try client.call(services.sts.get_caller_identity.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\Action=GetCallerIdentity&Version=2011-06-15 - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings( - "arn:aws:iam::123456789012:user/admin", - call.response.arn.?, - ); - try std.testing.expectEqualStrings("AIDAYAM4POHXHRVANDQBQ", call.response.user_id.?); - try std.testing.expectEqualStrings("123456789012", call.response.account.?); - try std.testing.expectEqualStrings("8f0d54da-1230-40f7-b4ac-95015c4b84cd", call.response_metadata.request_id); -} -test "query_with_input: iam getRole runtime" { - // sqs switched from query to json in aws sdk for go v2 commit f5a08768ef820ff5efd62a49ba50c61c9ca5dbcb - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\ - \\ - \\ - \\ /application_abc/component_xyz/ - \\ arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access - \\ S3Access - \\ - \\ {"Version":"2012-10-17","Statement":[{"Effect":"Allow", - \\ "Principal":{"Service":["ec2.amazonaws.com"]},"Action":["sts:AssumeRole"]}]} - \\ - \\ 2012-05-08T23:34:01Z - \\ AROADBQP57FF2AEXAMPLE - \\ - \\ 2019-11-20T17:09:20Z - \\ us-east-1 - \\ - \\ - \\ - \\ - \\ df37e965-9967-11e1-a4c3-270EXAMPLE04 - \\ - \\ - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "text/xml" }, - .{ .name = "x-amzn-RequestId", .value = "df37e965-9967-11e1-a4c3-270EXAMPLE04" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const iam = (Services(.{.iam}){}).iam; - const call = try test_harness.client.call(iam.get_role.Request{ - .role_name = "S3Access", - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\Action=GetRole&Version=2010-05-08&RoleName=S3Access - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access", call.response.role.arn); - try std.testing.expectEqualStrings("df37e965-9967-11e1-a4c3-270EXAMPLE04", call.response_metadata.request_id); -} -test "query_with_input: sts getAccessKeyInfo runtime" { - // sqs switched from query to json in aws sdk for go v2 commit f5a08768ef820ff5efd62a49ba50c61c9ca5dbcb - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\ - \\ - \\ 123456789012 - \\ - \\ - \\ ec85bf29-1ef0-459a-930e-6446dd14a286 - \\ - \\ - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "text/xml" }, - .{ .name = "x-amzn-RequestId", .value = "ec85bf29-1ef0-459a-930e-6446dd14a286" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const sts = (Services(.{.sts}){}).sts; - const call = try test_harness.client.call(sts.get_access_key_info.Request{ - .access_key_id = "ASIAYAM4POHXJNKTYFUN", - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\Action=GetAccessKeyInfo&Version=2011-06-15&AccessKeyId=ASIAYAM4POHXJNKTYFUN - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expect(call.response.account != null); - try std.testing.expectEqualStrings("123456789012", call.response.account.?); - try std.testing.expectEqualStrings("ec85bf29-1ef0-459a-930e-6446dd14a286", call.response_metadata.request_id); -} -test "json_1_0_query_with_input: dynamodb listTables runtime" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"LastEvaluatedTableName":"Customer","TableNames":["Customer"]} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const dynamo_db = services.dynamo_db; - const call = try test_harness.client.call(dynamo_db.list_tables.Request{ - .limit = 1, - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try test_harness.request_options.expectHeader("X-Amz-Target", "DynamoDB_20120810.ListTables"); - - const parsed_body = try std.json.parseFromSlice(struct { - ExclusiveStartTableName: ?[]const u8, - Limit: u8, - }, testing.allocator, test_harness.request_options.request_body, .{}); - defer parsed_body.deinit(); - - try testing.expectEqual(null, parsed_body.value.ExclusiveStartTableName); - try testing.expectEqual(1, parsed_body.value.Limit); - - // Response expectations - try std.testing.expectEqualStrings("QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 1), call.response.table_names.?.len); - try std.testing.expectEqualStrings("Customer", call.response.table_names.?[0]); -} - -test "json_1_0_query_no_input: dynamodb listTables runtime" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"AccountMaxReadCapacityUnits":80000,"AccountMaxWriteCapacityUnits":80000,"TableMaxReadCapacityUnits":40000,"TableMaxWriteCapacityUnits":40000} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const dynamo_db = (Services(.{.dynamo_db}){}).dynamo_db; - const call = try test_harness.client.call(dynamo_db.describe_limits.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try test_harness.request_options.expectHeader("X-Amz-Target", "DynamoDB_20120810.DescribeLimits"); - try std.testing.expectEqualStrings( - \\{} - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG", call.response_metadata.request_id); - try std.testing.expectEqual(@as(i64, 80000), call.response.account_max_read_capacity_units.?); -} -test "json_1_1_query_with_input: ecs listClusters runtime" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"clusterArns":["arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster"],"nextToken":"czE0Og=="} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "b2420066-ff67-4237-b782-721c4df60744" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const ecs = (Services(.{.ecs}){}).ecs; - const call = try test_harness.client.call(ecs.list_clusters.Request{ - .max_results = 1, - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try test_harness.request_options.expectHeader("X-Amz-Target", "AmazonEC2ContainerServiceV20141113.ListClusters"); - - const parsed_body = try std.json.parseFromSlice(struct { - nextToken: ?[]const u8, - maxResults: u8, - }, testing.allocator, test_harness.request_options.request_body, .{}); - defer parsed_body.deinit(); - - try testing.expectEqual(null, parsed_body.value.nextToken); - try testing.expectEqual(1, parsed_body.value.maxResults); - - // Response expectations - try std.testing.expectEqualStrings("b2420066-ff67-4237-b782-721c4df60744", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 1), call.response.cluster_arns.?.len); - try std.testing.expectEqualStrings("arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster", call.response.cluster_arns.?[0]); -} -test "json_1_1_query_no_input: ecs listClusters runtime" { - // const old = std.testing.log_level; - // defer std.testing.log_level = old; - // std.testing.log_level = .debug; - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"clusterArns":["arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster"],"nextToken":"czE0Og=="} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "e65322b2-0065-45f2-ba37-f822bb5ce395" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const ecs = (Services(.{.ecs}){}).ecs; - const call = try test_harness.client.call(ecs.list_clusters.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try test_harness.request_options.expectHeader("X-Amz-Target", "AmazonEC2ContainerServiceV20141113.ListClusters"); - - const parsed_body = try std.json.parseFromSlice(struct { - nextToken: ?[]const u8, - maxResults: ?u8, - }, testing.allocator, test_harness.request_options.request_body, .{}); - defer parsed_body.deinit(); - - try testing.expectEqual(null, parsed_body.value.nextToken); - try testing.expectEqual(null, parsed_body.value.maxResults); - - // Response expectations - try std.testing.expectEqualStrings("e65322b2-0065-45f2-ba37-f822bb5ce395", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 1), call.response.cluster_arns.?.len); - try std.testing.expectEqualStrings("arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster", call.response.cluster_arns.?[0]); -} -test "rest_json_1_query_with_input: lambda listFunctions runtime" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"Functions":[{"Description":"AWS CDK resource provider framework - onEvent (DevelopmentFrontendStack-g650u/com.amazonaws.cdk.custom-resources.amplify-asset-deployment-provider/amplify-asset-deployment-handler-provider)","TracingConfig":{"Mode":"PassThrough"},"VpcConfig":null,"SigningJobArn":null,"SnapStart":{"OptimizationStatus":"Off","ApplyOn":"None"},"RevisionId":"0c62fc74-a692-403d-9206-5fcbad406424","LastModified":"2023-03-01T18:13:15.704+0000","FileSystemConfigs":null,"FunctionName":"DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU","Runtime":"nodejs14.x","Version":"$LATEST","PackageType":"Zip","LastUpdateStatus":null,"Layers":null,"FunctionArn":"arn:aws:lambda:us-west-2:550620852718:function:DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU","KMSKeyArn":null,"MemorySize":128,"ImageConfigResponse":null,"LastUpdateStatusReason":null,"DeadLetterConfig":null,"Timeout":900,"Handler":"framework.onEvent","CodeSha256":"m4tt+M0l3p8bZvxIDj83dwGrwRW6atCfS/q8AiXCD3o=","Role":"arn:aws:iam::550620852718:role/DevelopmentFrontendStack-amplifyassetdeploymentha-1782JF7WAPXZ3","SigningProfileVersionArn":null,"MasterArn":null,"RuntimeVersionConfig":null,"CodeSize":4307,"State":null,"StateReason":null,"Environment":{"Variables":{"USER_ON_EVENT_FUNCTION_ARN":"arn:aws:lambda:us-west-2:550620852718:function:DevelopmentFrontendStack--amplifyassetdeploymenton-X9iZJSCSPYDH","WAITER_STATE_MACHINE_ARN":"arn:aws:states:us-west-2:550620852718:stateMachine:amplifyassetdeploymenthandlerproviderwaiterstatemachineB3C2FCBE-Ltggp5wBcHWO","USER_IS_COMPLETE_FUNCTION_ARN":"arn:aws:lambda:us-west-2:550620852718:function:DevelopmentFrontendStack--amplifyassetdeploymentis-jaHopLrSSARV"},"Error":null},"EphemeralStorage":{"Size":512},"StateReasonCode":null,"LastUpdateStatusReasonCode":null,"Architectures":["x86_64"]}],"NextMarker":"lslTXFcbLQKkb0vP9Kgh5hUL7C3VghELNGbWgZfxrRCk3eiDRMkct7D8EmptWfHSXssPdS7Bo66iQPTMpVOHZgANewpgGgFGGr4pVjd6VgLUO6qPe2EMAuNDBjUTxm8z6N28yhlUwEmKbrAV/m0k5qVzizwoxFwvyruMbuMx9kADFACSslcabxXl3/jDI4rfFnIsUVdzTLBgPF1hzwrE1f3lcdkBvUp+QgY+Pn3w5QuJmwsp/di8COzFemY89GgOHbLNqsrBsgR/ee2eXoJp0ZkKM4EcBK3HokqBzefLfgR02PnfNOdXwqTlhkSPW0TKiKGIYu3Bw7lSNrLd+q3+wEr7ZakqOQf0BVo3FMRhMHlVYgwUJzwi3ActyH2q6fuqGG1sS0B8Oa/prUpe5fmp3VaA3WpazioeHtrKF78JwCi6/nfQsrj/8ZtXGQOxlwEgvT1CIUaF+CdHY3biezrK0tRZNpkCtHnkPtF9lq2U7+UiKXSW9yzxT8P2b0M/Qh4IVdnw4rncQK/doYriAeOdrs1wjMEJnHWq9lAaEyipoxYcVr/z5+yaC6Gwxdg45p9X1vIAaYMf6IZxyFuua43SYi0Ls+IBk4VvpR2io7T0dCxHAr3WAo3D2dm0y8OsbM59"} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "c4025199-226f-4a16-bb1f-48618e9d2ea6" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const lambda = (Services(.{.lambda}){}).lambda; - const call = try test_harness.client.call(lambda.list_functions.Request{ - .max_items = 1, - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/2015-03-31/functions?MaxItems=1", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\ - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("c4025199-226f-4a16-bb1f-48618e9d2ea6", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 1), call.response.functions.?.len); - try std.testing.expectEqualStrings( - "DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU", - call.response.functions.?[0].function_name.?, - ); -} -test "rest_json_1_query_no_input: lambda listFunctions runtime" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = @embedFile("test_rest_json_1_query_no_input.response"), - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "b2aad11f-36fc-4d0d-ae92-fe0167fb0f40" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const lambda = (Services(.{.lambda}){}).lambda; - const call = try test_harness.client.call(lambda.list_functions.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/2015-03-31/functions", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\ - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("b2aad11f-36fc-4d0d-ae92-fe0167fb0f40", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 24), call.response.functions.?.len); - try std.testing.expectEqualStrings( - "DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU", - call.response.functions.?[0].function_name.?, - ); - try std.testing.expectEqualStrings( - "amplify-login-create-auth-challenge-b4883e4c", - call.response.functions.?[12].function_name.?, - ); -} -test "rest_json_1_work_with_lambda: lambda tagResource (only), to excercise zig issue 17015" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = "", - .server_response_status = .no_content, - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "a521e152-6e32-4e67-9fb3-abc94e34551b" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const lambda = (Services(.{.lambda}){}).lambda; - var tags = try std.ArrayList(@typeInfo(try typeForField(lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 1); - defer tags.deinit(allocator); - tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" }); - const req = services.lambda.tag_resource.Request{ .resource = "arn:aws:lambda:us-west-2:550620852718:function:awsome-lambda-LambdaStackawsomeLambda", .tags = tags.items }; - const call = try Request(lambda.tag_resource).call(req, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - - const parsed_body = try std.json.parseFromSlice(struct { - Tags: struct { - Foo: []const u8, - }, - }, testing.allocator, test_harness.request_options.request_body, .{ .ignore_unknown_fields = true }); - defer parsed_body.deinit(); - - try testing.expectEqualStrings("Bar", parsed_body.value.Tags.Foo); - - // Due to 17015, we see %253A instead of %3A - try std.testing.expectEqualStrings("/2017-03-31/tags/arn%3Aaws%3Alambda%3Aus-west-2%3A550620852718%3Afunction%3Aawsome-lambda-LambdaStackawsomeLambda", test_harness.request_options.request_target); - // Response expectations - try std.testing.expectEqualStrings("a521e152-6e32-4e67-9fb3-abc94e34551b", call.response_metadata.request_id); -} -test "rest_json_1_url_parameters_not_in_request: lambda update_function_code" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = "{\"CodeSize\": 42}", - .server_response_status = .ok, - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "a521e152-6e32-4e67-9fb3-abc94e34551b" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const lambda = (Services(.{.lambda}){}).lambda; - const architectures = [_][]const u8{"x86_64"}; - const arches: [][]const u8 = @constCast(architectures[0..]); - const req = services.lambda.update_function_code.Request{ - .function_name = "functionname", - .architectures = arches, - .zip_file = "zipfile", - }; - const call = try Request(lambda.update_function_code).call(req, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); - - const parsed_body = try std.json.parseFromSlice(struct { - ZipFile: []const u8, - Architectures: [][]const u8, - }, testing.allocator, test_harness.request_options.request_body, .{ - .ignore_unknown_fields = true, - }); - defer parsed_body.deinit(); - - try testing.expectEqualStrings("zipfile", parsed_body.value.ZipFile); - try testing.expectEqual(1, parsed_body.value.Architectures.len); - try testing.expectEqualStrings("x86_64", parsed_body.value.Architectures[0]); - - // Due to 17015, we see %253A instead of %3A - try std.testing.expectEqualStrings("/2015-03-31/functions/functionname/code", test_harness.request_options.request_target); - // Response expectations - try std.testing.expectEqualStrings("a521e152-6e32-4e67-9fb3-abc94e34551b", call.response_metadata.request_id); -} -test "ec2_query_no_input: EC2 describe regions" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = @embedFile("test_ec2_query_no_input.response"), - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "text/xml;charset=UTF-8" }, - .{ .name = "x-amzn-RequestId", .value = "4cdbdd69-800c-49b5-8474-ae4c17709782" }, - }, - .server_response_transfer_encoding = .chunked, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const ec2 = (Services(.{.ec2}){}).ec2; - const call = try test_harness.client.call(ec2.describe_regions.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/?Action=DescribeRegions&Version=2016-11-15", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\Action=DescribeRegions&Version=2016-11-15 - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("4cdbdd69-800c-49b5-8474-ae4c17709782", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 17), call.response.regions.?.len); -} -// LLVM hates this test. Depending on the platform, it will consume all memory -// on the compilation host. Windows x86_64 and Linux riscv64 seem to be a problem so far -// riscv64-linux also seems to have another problem with LLVM basically infinitely -// doing something. My guess is the @embedFile is freaking out LLVM -test "ec2_query_with_input: EC2 describe instances" { - if (builtin.cpu.arch == .riscv64 and builtin.os.tag == .linux) return error.SkipZigTest; - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = @embedFile("test_ec2_query_with_input.response"), - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "text/xml;charset=UTF-8" }, - .{ .name = "x-amzn-RequestId", .value = "150a14cc-785d-476f-a4c9-2aa4d03b14e2" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const ec2 = (Services(.{.ec2}){}).ec2; - const call = try test_harness.client.call(ec2.describe_instances.Request{ - .max_results = 6, - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/?Action=DescribeInstances&Version=2016-11-15", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\Action=DescribeInstances&Version=2016-11-15&MaxResults=6 - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("150a14cc-785d-476f-a4c9-2aa4d03b14e2", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 6), call.response.reservations.?.len); - try std.testing.expectEqualStrings("i-0212d7d1f62b96676", call.response.reservations.?[1].instances.?[0].instance_id.?); - try std.testing.expectEqualStrings("123456789012:found-me", call.response.reservations.?[1].instances.?[0].tags.?[0].value.?); -} -test "rest_xml_with_input_s3: S3 create bucket" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\ - , - .server_response_headers = &.{ // I don't see content type coming back in actual S3 requests - .{ .name = "x-amzn-RequestId", .value = "9PEYBAZ9J7TPRX43" }, - .{ .name = "x-amz-id-2", .value = "u7lzgW0tIyRP15vSUsVOXxJ37OfVCO8lZmLIVuqeq5EE4tNp9qebb5fy+/kendlZpR4YQE+y4Xg=" }, - }, - }); - defer test_harness.deinit(); - errdefer test_harness.creds.deinit(); - const options = try test_harness.start(); - const s3 = (Services(.{.s3}){}).s3; - const call = try test_harness.client.call(s3.create_bucket.Request{ - .bucket = "", - .create_bucket_configuration = .{ - .location_constraint = "us-west-2", - }, - }, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try std.testing.expectEqualStrings( - \\ - \\ us-west-2 - \\ - , test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings( - "9PEYBAZ9J7TPRX43, host_id: u7lzgW0tIyRP15vSUsVOXxJ37OfVCO8lZmLIVuqeq5EE4tNp9qebb5fy+/kendlZpR4YQE+y4Xg=", - call.response_metadata.request_id, - ); -} -test "rest_xml_no_input: S3 list buckets" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\3367189aa775bd98da38e55093705f2051443c1e775fc0971d6d77387a47c8d0emilerch+sub1550620852718-backup2020-06-17T16:26:51.000Zamplify-letmework-staging-185741-deployment2023-03-10T18:57:49.000Zaws-cloudtrail-logs-550620852718-224022a72021-06-21T18:32:44.000Zaws-sam-cli-managed-default-samclisourcebucket-1gy0z00mj47xe2021-10-05T16:38:07.000Zawsomeprojectstack-pipelineartifactsbucketaea9a05-1uzwo6c86ecr2021-10-05T22:55:09.000Zcdk-hnb659fds-assets-550620852718-us-west-22023-02-28T21:49:36.000Zcf-templates-12iy6putgdxtk-us-west-22020-06-26T02:31:59.000Zcodepipeline-us-west-2-467140836372021-09-14T18:43:07.000Zelasticbeanstalk-us-west-2-5506208527182022-04-15T16:22:42.000Zlobo-west2021-06-21T17:17:22.000Zlobo-west-22021-11-19T20:12:31.000Zlogging-backup-550620852718-us-east-22022-05-29T21:55:16.000Zmysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr02023-03-01T04:53:55.000Z - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/xml" }, - .{ .name = "x-amzn-RequestId", .value = "9PEYBAZ9J7TPRX43" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const s3 = (Services(.{.s3}){}).s3; - const call = try test_harness.client.call(s3.list_buckets.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); - // This changed in rev 830202d722c904c7e3da40e8dde7b9338d08752c of the go sdk, and - // contrary to the documentation, a query string argument was added. My guess is that - // there is no functional reason, and that this is strictly for some AWS reporting function. - // Alternatively, it could be to support some customization mechanism, as the commit - // title of that commit is "Merge customizations for S3" - try std.testing.expectEqualStrings("/?x-id=ListBuckets", test_harness.request_options.request_target); - try std.testing.expectEqualStrings("", test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("9PEYBAZ9J7TPRX43", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 13), call.response.buckets.?.len); -} -test "rest_xml_anything_but_s3: CloudFront list key groups" { - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"Items":null,"MaxItems":100,"NextMarker":null,"Quantity":0} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "d3382082-5291-47a9-876b-8df3accbb7ea" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const cloudfront = (Services(.{.cloudfront}){}).cloudfront; - const call = try test_harness.client.call(cloudfront.list_key_groups.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/2020-05-31/key-group", test_harness.request_options.request_target); - try std.testing.expectEqualStrings("", test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("d3382082-5291-47a9-876b-8df3accbb7ea", call.response_metadata.request_id); - try std.testing.expectEqual(@as(i64, 100), call.response.key_group_list.?.max_items); -} -test "rest_xml_with_input: S3 put object" { - // const old = std.testing.log_level; - // defer std.testing.log_level = old; - // std.testing.log_level = .debug; - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = "", - .server_response_headers = &.{ - // .{ "Content-Type", "application/xml" }, - .{ .name = "x-amzn-RequestId", .value = "9PEYBAZ9J7TPRX43" }, - .{ .name = "x-amz-id-2", .value = "jdRDo30t7Ge9lf6F+4WYpg+YKui8z0mz2+rwinL38xDZzvloJqrmpCAiKG375OSvHA9OBykJS44=" }, - .{ .name = "x-amz-server-side-encryption", .value = "AES256" }, - .{ .name = "ETag", .value = "37b51d194a7513e45b56f6524f2d51f2" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const s3opts = Options{ - .region = "us-west-2", - .client = options.client, - .signing_time = TestSetup.signing_time, - }; - const result = try Request(services.s3.put_object).call(.{ - .bucket = "mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0", - .key = "i/am/a/teapot/foo", - .content_type = "text/plain", - .body = "bar", - .storage_class = "STANDARD", - }, s3opts); - defer result.deinit(); - for (test_harness.request_options.request_headers) |header| { - std.log.info("Request header: {s}: {s}", .{ header.name, header.value }); - } - try test_harness.request_options.expectNoDuplicateHeaders(); - std.log.info("PutObject Request id: {s}", .{result.response_metadata.request_id}); - std.log.info("PutObject etag: {s}", .{result.response.e_tag.?}); - //mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0.s3.us-west-2.amazonaws.com - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); - // I don't think this will work since we're overriding the url - // try test_harness.request_options.expectHeader("Host", "mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0.s3.us-west-2.amazonaws.com"); - try test_harness.request_options.expectHeader("x-amz-storage-class", "STANDARD"); - try std.testing.expectEqualStrings("/mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0/i/am/a/teapot/foo?x-id=PutObject", test_harness.request_options.request_target); - try std.testing.expectEqualStrings("bar", test_harness.request_options.request_body); - // Response expectations - try std.testing.expectEqualStrings("9PEYBAZ9J7TPRX43, host_id: jdRDo30t7Ge9lf6F+4WYpg+YKui8z0mz2+rwinL38xDZzvloJqrmpCAiKG375OSvHA9OBykJS44=", result.response_metadata.request_id); - try std.testing.expectEqualStrings("AES256", result.response.server_side_encryption.?); - try std.testing.expectEqualStrings("37b51d194a7513e45b56f6524f2d51f2", result.response.e_tag.?); -} -test "raw ECR timestamps" { - // This is a way to test the json parsing. Ultimately the more robust tests - // should be preferred, but in this case we were tracking down an issue - // for which the root cause was the incorrect type being passed to the parse - // routine - const allocator = std.testing.allocator; - const ecr = (Services(.{.ecr}){}).ecr; - const options = json.ParseOptions{ - .allocator = allocator, - .allow_camel_case_conversion = true, // new option - .allow_snake_case_conversion = true, // new option - .allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though - .allow_missing_fields = false, // new option. Cannot yet handle non-struct fields though - }; - var stream = json.TokenStream.init( - \\{"authorizationData":[{"authorizationToken":"***","expiresAt":1.7385984915E9,"proxyEndpoint":"https://146325435496.dkr.ecr.us-west-2.amazonaws.com"}]} - ); - const ptr = try json.parse(ecr.get_authorization_token.Response, &stream, options); - defer json.parseFree(ecr.get_authorization_token.Response, ptr, options); -} -test "json_1_1: ECR timestamps" { - // See: https://github.com/elerch/aws-sdk-for-zig/issues/5 - // const old = std.testing.log_level; - // defer std.testing.log_level = old; - // std.testing.log_level = .debug; - const allocator = std.testing.allocator; - - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{"authorizationData":[{"authorizationToken":"***","expiresAt":"2022-05-17T06:56:13.652000+00:00","proxyEndpoint":"https://146325435496.dkr.ecr.us-west-2.amazonaws.com"}]} - // \\{"authorizationData":[{"authorizationToken":"***","expiresAt":1.738598491557E9,"proxyEndpoint":"https://146325435496.dkr.ecr.us-west-2.amazonaws.com"}]} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, - }, - }); - defer test_harness.deinit(); - const options = try test_harness.start(); - const ecr = (Services(.{.ecr}){}).ecr; - std.log.debug("Typeof response {}", .{@TypeOf(ecr.get_authorization_token.Response{})}); - const call = try test_harness.client.call(ecr.get_authorization_token.Request{}, options); - defer call.deinit(); - test_harness.stop(); - // Request expectations - try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); - try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); - try test_harness.request_options.expectHeader("X-Amz-Target", "AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken"); - // Response expectations - try std.testing.expectEqualStrings("QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG", call.response_metadata.request_id); - try std.testing.expectEqual(@as(usize, 1), call.response.authorization_data.?.len); - try std.testing.expectEqualStrings("***", call.response.authorization_data.?[0].authorization_token.?); - try std.testing.expectEqualStrings("https://146325435496.dkr.ecr.us-west-2.amazonaws.com", call.response.authorization_data.?[0].proxy_endpoint.?); - // try std.testing.expectEqual(@as(i64, 1.73859841557E9), call.response.authorization_data.?[0].expires_at.?); - - const expected_ins = try zeit.instant(.{ - .source = .{ .iso8601 = "2022-05-17T06:56:13.652000+00:00" }, - }); - const expected_ts: date.Timestamp = @enumFromInt(expected_ins.timestamp); - - try std.testing.expectEqual(expected_ts, call.response.authorization_data.?[0].expires_at.?); -} -var test_error_log_enabled = true; -test "test server timeout works" { - // const old = std.testing.log_level; - // defer std.testing.log_level = old; - // std.testing.log_level = .debug; - // defer std.testing.log_level = old; - // std.testing.log_level = .debug; - test_error_log_enabled = false; - defer test_error_log_enabled = true; - std.log.debug("test start", .{}); - const allocator = std.testing.allocator; - var test_harness = TestSetup.init(.{ - .allocator = allocator, - .server_response = - \\{} - , - .server_response_headers = &.{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, - }, - }); - defer test_harness.deinit(); - defer test_harness.creds.deinit(); // Usually this gets done during the call, - // but we're purposely not making a call - // here, so we have to deinit() manually - _ = try test_harness.start(); - std.log.debug("harness started", .{}); - test_harness.stop(); - std.log.debug("test complete", .{}); -} - -const testing = std.testing; - -test "jsonStringify: structure + enums" { - const request = services.media_convert.PutPolicyRequest{ - .policy = .{ - .http_inputs = "foo", - .https_inputs = "bar", - .s3_inputs = "baz", - }, - }; - - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - - const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); - defer std.testing.allocator.free(request_json); - - const parsed = try std.json.parseFromSlice(struct { - policy: struct { - httpInputs: []const u8, - httpsInputs: []const u8, - s3Inputs: []const u8, - }, - }, testing.allocator, request_json, .{}); - defer parsed.deinit(); - - try testing.expectEqualStrings("foo", parsed.value.policy.httpInputs); - try testing.expectEqualStrings("bar", parsed.value.policy.httpsInputs); - try testing.expectEqualStrings("baz", parsed.value.policy.s3Inputs); -} - -test "jsonStringify: strings" { - const request = services.media_convert.AssociateCertificateRequest{ - .arn = "1234", - }; - - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - - const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); - defer std.testing.allocator.free(request_json); - - try testing.expectEqualStrings("{\"arn\":\"1234\"}", request_json); -} - -test "jsonStringify" { - var tags = [_]services.media_convert.MapOfStringKeyValue{ - .{ - .key = "foo", - .value = "bar", - }, - }; - - const request = services.media_convert.TagResourceRequest{ - .arn = "1234", - .tags = &tags, - }; - - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - - const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); - defer std.testing.allocator.free(request_json); - - const json_parsed = try std.json.parseFromSlice(struct { - arn: []const u8, - tags: struct { - foo: []const u8, - }, - }, testing.allocator, request_json, .{}); - defer json_parsed.deinit(); - - try testing.expectEqualStrings("1234", json_parsed.value.arn); - try testing.expectEqualStrings("bar", json_parsed.value.tags.foo); -} - -test "jsonStringify nullable object" { - // structure is not null - { - const request = services.lambda.CreateAliasRequest{ - .function_name = "foo", - .function_version = "bar", - .name = "baz", - .routing_config = services.lambda.AliasRoutingConfiguration{ - .additional_version_weights = null, - }, - }; - - const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); - defer std.testing.allocator.free(request_json); - - const json_parsed = try std.json.parseFromSlice(struct { - FunctionName: []const u8, - FunctionVersion: []const u8, - Name: []const u8, - RoutingConfig: struct { - AdditionalVersionWeights: ?struct {}, - }, - }, testing.allocator, request_json, .{ .ignore_unknown_fields = true }); - defer json_parsed.deinit(); - - try testing.expectEqualStrings("foo", json_parsed.value.FunctionName); - try testing.expectEqualStrings("bar", json_parsed.value.FunctionVersion); - try testing.expectEqualStrings("baz", json_parsed.value.Name); - try testing.expectEqual(null, json_parsed.value.RoutingConfig.AdditionalVersionWeights); - } - - // structure is null - { - const request = services.kms.DecryptRequest{ - .key_id = "foo", - .ciphertext_blob = "bar", - }; - - const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); - defer std.testing.allocator.free(request_json); - - const json_parsed = try std.json.parseFromSlice(struct { - KeyId: []const u8, - CiphertextBlob: []const u8, - }, testing.allocator, request_json, .{ .ignore_unknown_fields = true }); - defer json_parsed.deinit(); - - try testing.expectEqualStrings("foo", json_parsed.value.KeyId); - try testing.expectEqualStrings("bar", json_parsed.value.CiphertextBlob); - } } diff --git a/src/aws_test.zig b/src/aws_test.zig new file mode 100644 index 0000000..ad29522 --- /dev/null +++ b/src/aws_test.zig @@ -0,0 +1,1410 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const date = @import("date"); +const json = @import("json"); + +const aws = @import("aws.zig"); +const awshttp = @import("aws_http.zig"); + +const services = aws.servicemodel.services; +const Services = aws.servicemodel.Services; + +const log = std.log.scoped(.aws_test); + +pub var buildQuery: u8 = undefined; +pub var buildPath: u8 = undefined; + +// TODO: Where does this belong really? +fn typeForField(comptime T: type, comptime field_name: []const u8) !type { + const ti = @typeInfo(T); + switch (ti) { + .@"struct" => { + inline for (ti.@"struct".fields) |field| { + if (std.mem.eql(u8, field.name, field_name)) + return field.type; + } + }, + else => return error.TypeIsNotAStruct, // should not hit this + } + return error.FieldNotFound; +} + +test "custom serialization for map objects" { + const allocator = std.testing.allocator; + var buffer = std.Io.Writer.Allocating.init(allocator); + defer buffer.deinit(); + var tags = try std.ArrayList(@typeInfo(try typeForField(services.lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 2); + defer tags.deinit(allocator); + tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" }); + tags.appendAssumeCapacity(.{ .key = "Baz", .value = "Qux" }); + const req = services.lambda.TagResourceRequest{ .resource = "hello", .tags = tags.items }; + try buffer.writer.print("{f}", .{std.json.fmt(req, .{ .whitespace = .indent_4 })}); + + const parsed_body = try std.json.parseFromSlice(struct { + Resource: []const u8, + Tags: struct { + Foo: []const u8, + Baz: []const u8, + }, + }, testing.allocator, buffer.written(), .{}); + defer parsed_body.deinit(); + + try testing.expectEqualStrings("hello", parsed_body.value.Resource); + try testing.expectEqualStrings("Bar", parsed_body.value.Tags.Foo); + try testing.expectEqualStrings("Qux", parsed_body.value.Tags.Baz); +} + +test "proper serialization for kms" { + // Github issue #8 + // https://github.com/elerch/aws-sdk-for-zig/issues/8 + const allocator = std.testing.allocator; + var buffer = std.Io.Writer.Allocating.init(allocator); + defer buffer.deinit(); + const req = services.kms.encrypt.Request{ + .encryption_algorithm = "SYMMETRIC_DEFAULT", + // Since encryption_context is not null, we expect "{}" to be the value + // here, not "[]", because this is our special AWS map pattern + .encryption_context = &.{}, + .key_id = "42", + .plaintext = "foo", + .dry_run = false, + .grant_tokens = &[_][]const u8{}, + }; + try buffer.writer.print("{f}", .{std.json.fmt(req, .{ .whitespace = .indent_4 })}); + + { + const parsed_body = try std.json.parseFromSlice(struct { + KeyId: []const u8, + Plaintext: []const u8, + EncryptionContext: struct {}, + GrantTokens: [][]const u8, + EncryptionAlgorithm: []const u8, + DryRun: bool, + }, testing.allocator, buffer.written(), .{}); + defer parsed_body.deinit(); + + try testing.expectEqualStrings("42", parsed_body.value.KeyId); + try testing.expectEqualStrings("foo", parsed_body.value.Plaintext); + try testing.expectEqual(0, parsed_body.value.GrantTokens.len); + try testing.expectEqualStrings("SYMMETRIC_DEFAULT", parsed_body.value.EncryptionAlgorithm); + try testing.expectEqual(false, parsed_body.value.DryRun); + } + + var buffer_null = std.Io.Writer.Allocating.init(allocator); + defer buffer_null.deinit(); + const req_null = services.kms.encrypt.Request{ + .encryption_algorithm = "SYMMETRIC_DEFAULT", + // Since encryption_context here *IS* null, we expect simply "null" to be the value + .encryption_context = null, + .key_id = "42", + .plaintext = "foo", + .dry_run = false, + .grant_tokens = &[_][]const u8{}, + }; + + try buffer_null.writer.print("{f}", .{std.json.fmt(req_null, .{ .whitespace = .indent_4 })}); + + { + const parsed_body = try std.json.parseFromSlice(struct { + KeyId: []const u8, + Plaintext: []const u8, + EncryptionContext: ?struct {}, + GrantTokens: [][]const u8, + EncryptionAlgorithm: []const u8, + DryRun: bool, + }, testing.allocator, buffer_null.written(), .{}); + defer parsed_body.deinit(); + + try testing.expectEqualStrings("42", parsed_body.value.KeyId); + try testing.expectEqualStrings("foo", parsed_body.value.Plaintext); + try testing.expectEqual(null, parsed_body.value.EncryptionContext); + try testing.expectEqual(0, parsed_body.value.GrantTokens.len); + try testing.expectEqualStrings("SYMMETRIC_DEFAULT", parsed_body.value.EncryptionAlgorithm); + try testing.expectEqual(false, parsed_body.value.DryRun); + } +} + +test "REST Json v1 builds proper queries" { + const allocator = std.testing.allocator; + const svs = Services(.{.lambda}){}; + const request = svs.lambda.list_functions.Request{ + .max_items = 1, + }; + const query = try buildQuery(allocator, request); + defer allocator.free(query); + try std.testing.expectEqualStrings("?MaxItems=1", query); +} +test "REST Json v1 handles reserved chars in queries" { + const allocator = std.testing.allocator; + const svs = Services(.{.lambda}){}; + var keys = [_][]const u8{"Foo?I'm a crazy%dude"}; // Would love to have a way to express this without burning a var here + const request = svs.lambda.untag_resource.Request{ + .tag_keys = keys[0..], + .resource = "hello", + }; + const query = try buildQuery(allocator, request); + defer allocator.free(query); + try std.testing.expectEqualStrings("?tagKeys=Foo%3FI%27m a crazy%25dude", query); +} +test "REST Json v1 serializes lists in queries" { + const allocator = std.testing.allocator; + const svs = Services(.{.lambda}){}; + var keys = [_][]const u8{ "Foo", "Bar" }; // Would love to have a way to express this without burning a var here + const request = svs.lambda.untag_resource.Request{ + .tag_keys = keys[0..], + .resource = "hello", + }; + const query = try buildQuery(allocator, request); + defer allocator.free(query); + try std.testing.expectEqualStrings("?tagKeys=Foo&tagKeys=Bar", query); +} +test "REST Json v1 buildpath substitutes" { + const allocator = std.testing.allocator; + var al = std.ArrayList([]const u8){}; + defer al.deinit(allocator); + const svs = Services(.{.lambda}){}; + const request = svs.lambda.list_functions.Request{ + .max_items = 1, + }; + const input_path = "https://myhost/{MaxItems}/"; + const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true, &al); + defer allocator.free(output_path); + try std.testing.expectEqualStrings("https://myhost/1/", output_path); +} +test "REST Json v1 buildpath handles restricted characters" { + const allocator = std.testing.allocator; + var al = std.ArrayList([]const u8){}; + defer al.deinit(allocator); + const svs = Services(.{.lambda}){}; + const request = svs.lambda.list_functions.Request{ + .marker = ":", + }; + const input_path = "https://myhost/{Marker}/"; + const output_path = try buildPath(allocator, input_path, @TypeOf(request), request, true, &al); + defer allocator.free(output_path); + try std.testing.expectEqualStrings("https://myhost/%3A/", output_path); +} +test "basic json request serialization" { + const allocator = std.testing.allocator; + const svs = Services(.{.dynamo_db}){}; + const request = svs.dynamo_db.list_tables.Request{ + .limit = 1, + }; + var buffer = std.Io.Writer.Allocating.init(allocator); + defer buffer.deinit(); + + // The transformer needs to allocate stuff out of band, but we + // can guarantee we don't need the memory after this call completes, + // so we'll use an arena allocator to whack everything. + // TODO: Determine if sending in null values is ok, or if we need another + // tweak to the stringify function to exclude. According to the + // smithy spec, "A null value MAY be provided or omitted + // for a boxed member with no observable difference." But we're + // seeing a lot of differences here between spec and reality + // + try buffer.writer.print("{f}", .{std.json.fmt(request, .{ .whitespace = .indent_4 })}); + try std.testing.expectEqualStrings( + \\{ + \\ "ExclusiveStartTableName": null, + \\ "Limit": 1 + \\} + , buffer.written()); +} +test "layer object only" { + const TestResponse = struct { + arn: ?[]const u8 = null, + // uncompressed_code_size: ?i64 = null, + + pub fn jsonFieldNameFor(_: @This(), comptime field_name: []const u8) []const u8 { + const mappings = .{ + .arn = "Arn", + }; + return @field(mappings, field_name); + } + }; + const response = + \\ { + \\ "UncompressedCodeSize": 2, + \\ "Arn": "blah" + \\ } + ; + // const response = + // \\ { + // \\ "UncompressedCodeSize": 22599541, + // \\ "Arn": "arn:aws:lambda:us-west-2:123456789012:layer:PollyNotes-lib:4" + // \\ } + // ; + const allocator = std.testing.allocator; + var stream = json.TokenStream.init(response); + const parser_options = json.ParseOptions{ + .allocator = allocator, + .allow_camel_case_conversion = true, // new option + .allow_snake_case_conversion = true, // new option + .allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though + .allow_missing_fields = false, // new option. Cannot yet handle non-struct fields though + }; + const r = try json.parse(TestResponse, &stream, parser_options); + json.parseFree(TestResponse, r, parser_options); +} + +// Use for debugging json responses of specific requests +// test "dummy request" { +// const allocator = std.testing.allocator; +// const svs = Services(.{.sts}){}; +// const request = svs.sts.get_session_token.Request{ +// .duration_seconds = 900, +// }; +// const FullR = FullResponse(request); +// const response = +// var stream = json.TokenStream.init(response); +// +// const parser_options = json.ParseOptions{ +// .allocator = allocator, +// .allow_camel_case_conversion = true, // new option +// .allow_snake_case_conversion = true, // new option +// .allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though +// .allow_missing_fields = false, // new option. Cannot yet handle non-struct fields though +// }; +// const SResponse = ServerResponse(request); +// const r = try json.parse(SResponse, &stream, parser_options); +// json.parseFree(SResponse, r, parser_options); + +test { + // To run nested container tests, either, call `refAllDecls` which will + // reference all declarations located in the given argument. + // `@This()` is a builtin function that returns the innermost container it is called from. + // In this example, the innermost container is this file (implicitly a struct). + std.testing.refAllDecls(@This()); + std.testing.refAllDecls(awshttp); + std.testing.refAllDecls(json); + std.testing.refAllDecls(@import("url.zig")); + std.testing.refAllDecls(@import("case")); + std.testing.refAllDecls(date); + std.testing.refAllDecls(@import("servicemodel.zig")); + std.testing.refAllDecls(@import("xml_shaper.zig")); +} +const TestOptions = struct { + allocator: std.mem.Allocator, + arena: ?*std.mem.ArenaAllocator = null, + server_port: ?u16 = null, + server_remaining_requests: usize = 1, + server_response: []const u8 = "unset", + server_response_status: std.http.Status = .ok, + server_response_headers: []const std.http.Header = &.{}, + server_response_transfer_encoding: ?std.http.TransferEncoding = null, + request_body: []u8 = "", + request_method: std.http.Method = undefined, + request_target: []const u8 = undefined, + request_headers: []std.http.Header = undefined, + test_server_runtime_uri: ?[]u8 = null, + server_ready: std.Thread.Semaphore = .{}, + requests_processed: usize = 0, + + const Self = @This(); + + /// Builtin hashmap for strings as keys. + /// Key memory is managed by the caller. Keys and values + /// will not automatically be freed. + pub fn StringCaseInsensitiveHashMap(comptime V: type) type { + return std.HashMap([]const u8, V, StringInsensitiveContext, std.hash_map.default_max_load_percentage); + } + + pub const StringInsensitiveContext = struct { + pub fn hash(self: @This(), s: []const u8) u64 { + _ = self; + return hashString(s); + } + pub fn eql(self: @This(), a: []const u8, b: []const u8) bool { + _ = self; + return eqlString(a, b); + } + }; + + pub fn eqlString(a: []const u8, b: []const u8) bool { + return std.ascii.eqlIgnoreCase(a, b); + } + + pub fn hashString(s: []const u8) u64 { + var buf: [1024]u8 = undefined; + if (s.len > buf.len) unreachable; // tolower has a debug assert, but we want non-debug check too + const lower_s = std.ascii.lowerString(buf[0..], s); + return std.hash.Wyhash.hash(0, lower_s); + } + + fn expectNoDuplicateHeaders(self: *Self) !void { + // As header keys are + var hm = StringCaseInsensitiveHashMap(void).init(self.allocator); + try hm.ensureTotalCapacity(@intCast(self.request_headers.len)); + defer hm.deinit(); + for (self.request_headers) |h| { + if (hm.getKey(h.name)) |_| { + log.err("Duplicate key detected. Key name: {s}", .{h.name}); + return error.duplicateKeyDetected; + } + try hm.put(h.name, {}); + } + } + + fn expectHeader(self: *Self, name: []const u8, value: []const u8) !void { + for (self.request_headers) |h| + if (std.ascii.eqlIgnoreCase(name, h.name) and + std.mem.eql(u8, value, h.value)) return; + return error.HeaderOrValueNotFound; + } + fn waitForReady(self: *Self) !void { + // Set 10s timeout...this is way longer than necessary + log.debug("waiting for ready", .{}); + try self.server_ready.timedWait(1000 * std.time.ns_per_ms); + // var deadline = std.Thread.Futex.Deadline.init(1000 * std.time.ns_per_ms); + // if (self.futex_word.load(.acquire) != 0) return; + // log.debug("futex zero", .{}); + // // note that this seems backwards from the documentation... + // deadline.wait(self.futex_word, 1) catch { + // log.err("futex value {d}", .{self.futex_word.load(.acquire)}); + // return error.TestServerTimeoutWaitingForReady; + // }; + log.debug("the wait is over!", .{}); + } +}; + +/// This starts a test server. We're not testing the server itself, +/// so the main tests will start this thing up and create an arena around the +/// whole thing so we can just deallocate everything at once at the end, +/// leaks be damned +fn threadMain(options: *TestOptions) !void { + // https://github.com/ziglang/zig/blob/d2be725e4b14c33dbd39054e33d926913eee3cd4/lib/compiler/std-docs.zig#L22-L54 + + options.arena = try options.allocator.create(std.mem.ArenaAllocator); + options.arena.?.* = std.heap.ArenaAllocator.init(options.allocator); + const allocator = options.arena.?.allocator(); + options.allocator = allocator; + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var http_server = try address.listen(.{}); + options.server_port = http_server.listen_address.in.getPort(); + // TODO: remove + options.test_server_runtime_uri = try std.fmt.allocPrint(options.allocator, "http://127.0.0.1:{d}", .{options.server_port.?}); + log.debug("server listening at {s}", .{options.test_server_runtime_uri.?}); + log.info("starting server thread, tid {d}", .{std.Thread.getCurrentId()}); + // var arena = std.heap.ArenaAllocator.init(options.allocator); + // defer arena.deinit(); + // var aa = arena.allocator(); + // We're in control of all requests/responses, so this flag will tell us + // when it's time to shut down + if (options.server_remaining_requests == 0) + options.server_ready.post(); // This will cause the wait for server to return + while (options.server_remaining_requests > 0) : (options.server_remaining_requests -= 1) { + processRequest(options, &http_server) catch |e| { + log.err("Unexpected error processing request: {any}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + }; + } +} + +fn processRequest(options: *TestOptions, net_server: *std.net.Server) !void { + log.debug( + "tid {d} (server): server waiting to accept. requests remaining: {d}", + .{ std.Thread.getCurrentId(), options.server_remaining_requests }, + ); + // options.futex_word.store(1, .release); + // errdefer options.futex_word.store(0, .release); + options.server_ready.post(); + var connection = try net_server.accept(); + defer connection.stream.close(); + var read_buffer: [1024 * 16]u8 = undefined; + var http_server = std.http.Server.init(connection, &read_buffer); + while (http_server.state == .ready) { + var request = http_server.receiveHead() catch |err| switch (err) { + error.HttpConnectionClosing => return, + else => { + std.log.err("closing http connection: {s}", .{@errorName(err)}); + std.log.debug("Error occurred from this request: \n{s}", .{read_buffer[0..http_server.read_buffer_len]}); + return; + }, + }; + try serveRequest(options, &request); + } +} + +fn serveRequest(options: *TestOptions, request: *std.http.Server.Request) !void { + options.requests_processed += 1; + options.request_body = try (try request.reader()).readAllAlloc(options.allocator, std.math.maxInt(usize)); + options.request_method = request.head.method; + options.request_target = try options.allocator.dupe(u8, request.head.target); + var req_headers = std.ArrayList(std.http.Header).init(options.allocator); + defer req_headers.deinit(); + var it = request.iterateHeaders(); + while (it.next()) |f| { + const h = try options.allocator.create(std.http.Header); + h.* = .{ .name = try options.allocator.dupe(u8, f.name), .value = try options.allocator.dupe(u8, f.value) }; + try req_headers.append(h.*); + } + options.request_headers = try req_headers.toOwnedSlice(); + log.debug( + "tid {d} (server): {d} bytes read from request", + .{ std.Thread.getCurrentId(), options.request_body.len }, + ); + + // try response.headers.append("content-type", "text/plain"); + try request.respond(options.server_response, .{ + .status = options.server_response_status, + .extra_headers = options.server_response_headers, + }); + + log.debug( + "tid {d} (server): sent response", + .{std.Thread.getCurrentId()}, + ); +} + +//////////////////////////////////////////////////////////////////////// +// These will replicate the tests that were in src/main.zig +// The server_response and server_response_headers come from logs of +// a previous run of src/main.zig, with redactions +//////////////////////////////////////////////////////////////////////// + +const TestSetup = struct { + allocator: std.mem.Allocator, + request_options: TestOptions, + server_thread: std.Thread = undefined, + creds: aws_auth.Credentials = undefined, + client: aws.Client = undefined, + started: bool = false, + + const Self = @This(); + + const aws_creds = @import("aws_credentials.zig"); + const aws_auth = @import("aws_authentication.zig"); + const signing_time = + date.dateTimeToTimestamp(date.parseIso8601ToDateTime("20230908T170252Z") catch @compileError("Cannot parse date")) catch @compileError("Cannot parse date"); + + fn init(options: TestOptions) Self { + return .{ + .request_options = options, + .allocator = options.allocator, + }; + } + + fn start(self: *Self) !aws.Options { + self.server_thread = try std.Thread.spawn( + .{}, + threadMain, + .{&self.request_options}, + ); + self.started = true; + try self.request_options.waitForReady(); + // Not sure why we're getting sprayed here, but we have an arena allocator, and this + // is testing, so yolo + awshttp.endpoint_override = self.request_options.test_server_runtime_uri; + if (awshttp.endpoint_override == null) return error.TestSetupStartFailure; + std.log.debug("endpoint override set to {?s}", .{awshttp.endpoint_override}); + self.creds = aws_auth.Credentials.init( + self.allocator, + try self.allocator.dupe(u8, "ACCESS"), + try self.allocator.dupe(u8, "SECRET"), + null, + ); + aws_creds.static_credentials = self.creds; + const client = aws.Client.init(self.allocator, .{}); + self.client = client; + return .{ + .region = "us-west-2", + .client = client, + .signing_time = signing_time, + }; + } + + fn stop(self: *Self) void { + if (self.request_options.server_remaining_requests > 0) + if (test_error_log_enabled) + std.log.err( + "Test server has {d} request(s) remaining to issue! Draining", + .{self.request_options.server_remaining_requests}, + ) + else + std.log.info( + "Test server has {d} request(s) remaining to issue! Draining", + .{self.request_options.server_remaining_requests}, + ); + + var rr = self.request_options.server_remaining_requests; + while (rr > 0) : (rr -= 1) { + std.log.debug("rr: {d}", .{self.request_options.server_remaining_requests}); + // We need to drain all remaining requests, otherwise the server + // will hang indefinitely + var client = std.http.Client{ .allocator = self.allocator }; + defer client.deinit(); + _ = client.fetch(.{ .location = .{ .url = self.request_options.test_server_runtime_uri.? } }) catch unreachable; + } + self.server_thread.join(); + } + + fn deinit(self: *Self) void { + if (self.request_options.arena) |a| { + a.deinit(); + self.allocator.destroy(a); + } + if (!self.started) return; + awshttp.endpoint_override = null; + // creds.deinit(); Creds will get deinited in the course of the call. We don't want to do it twice + aws_creds.static_credentials = null; // we do need to reset the static creds for the next user though + self.client.deinit(); + } +}; + +test "query_no_input: sts getCallerIdentity comptime" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"GetCallerIdentityResponse":{"GetCallerIdentityResult":{"Account":"123456789012","Arn":"arn:aws:iam::123456789012:user/admin","UserId":"AIDAYAM4POHXHRVANDQBQ"},"ResponseMetadata":{"RequestId":"8f0d54da-1230-40f7-b4ac-95015c4b84cd"}}} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "8f0d54da-1230-40f7-b4ac-95015c4b84cd" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const sts = (Services(.{.sts}){}).sts; + const call = try aws.Request(sts.get_caller_identity).call(.{}, options); + // const call = try client.call(services.sts.get_caller_identity.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\Action=GetCallerIdentity&Version=2011-06-15 + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings( + "arn:aws:iam::123456789012:user/admin", + call.response.arn.?, + ); + try std.testing.expectEqualStrings("AIDAYAM4POHXHRVANDQBQ", call.response.user_id.?); + try std.testing.expectEqualStrings("123456789012", call.response.account.?); + try std.testing.expectEqualStrings("8f0d54da-1230-40f7-b4ac-95015c4b84cd", call.response_metadata.request_id); +} +test "query_with_input: iam getRole runtime" { + // sqs switched from query to json in aws sdk for go v2 commit f5a08768ef820ff5efd62a49ba50c61c9ca5dbcb + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\ + \\ + \\ + \\ /application_abc/component_xyz/ + \\ arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access + \\ S3Access + \\ + \\ {"Version":"2012-10-17","Statement":[{"Effect":"Allow", + \\ "Principal":{"Service":["ec2.amazonaws.com"]},"Action":["sts:AssumeRole"]}]} + \\ + \\ 2012-05-08T23:34:01Z + \\ AROADBQP57FF2AEXAMPLE + \\ + \\ 2019-11-20T17:09:20Z + \\ us-east-1 + \\ + \\ + \\ + \\ + \\ df37e965-9967-11e1-a4c3-270EXAMPLE04 + \\ + \\ + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "text/xml" }, + .{ .name = "x-amzn-RequestId", .value = "df37e965-9967-11e1-a4c3-270EXAMPLE04" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const iam = (Services(.{.iam}){}).iam; + const call = try test_harness.client.call(iam.get_role.Request{ + .role_name = "S3Access", + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\Action=GetRole&Version=2010-05-08&RoleName=S3Access + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access", call.response.role.arn); + try std.testing.expectEqualStrings("df37e965-9967-11e1-a4c3-270EXAMPLE04", call.response_metadata.request_id); +} +test "query_with_input: sts getAccessKeyInfo runtime" { + // sqs switched from query to json in aws sdk for go v2 commit f5a08768ef820ff5efd62a49ba50c61c9ca5dbcb + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\ + \\ + \\ 123456789012 + \\ + \\ + \\ ec85bf29-1ef0-459a-930e-6446dd14a286 + \\ + \\ + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "text/xml" }, + .{ .name = "x-amzn-RequestId", .value = "ec85bf29-1ef0-459a-930e-6446dd14a286" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const sts = (Services(.{.sts}){}).sts; + const call = try test_harness.client.call(sts.get_access_key_info.Request{ + .access_key_id = "ASIAYAM4POHXJNKTYFUN", + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\Action=GetAccessKeyInfo&Version=2011-06-15&AccessKeyId=ASIAYAM4POHXJNKTYFUN + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expect(call.response.account != null); + try std.testing.expectEqualStrings("123456789012", call.response.account.?); + try std.testing.expectEqualStrings("ec85bf29-1ef0-459a-930e-6446dd14a286", call.response_metadata.request_id); +} +test "json_1_0_query_with_input: dynamodb listTables runtime" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"LastEvaluatedTableName":"Customer","TableNames":["Customer"]} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const dynamo_db = services.dynamo_db; + const call = try test_harness.client.call(dynamo_db.list_tables.Request{ + .limit = 1, + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try test_harness.request_options.expectHeader("X-Amz-Target", "DynamoDB_20120810.ListTables"); + + const parsed_body = try std.json.parseFromSlice(struct { + ExclusiveStartTableName: ?[]const u8, + Limit: u8, + }, testing.allocator, test_harness.request_options.request_body, .{}); + defer parsed_body.deinit(); + + try testing.expectEqual(null, parsed_body.value.ExclusiveStartTableName); + try testing.expectEqual(1, parsed_body.value.Limit); + + // Response expectations + try std.testing.expectEqualStrings("QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 1), call.response.table_names.?.len); + try std.testing.expectEqualStrings("Customer", call.response.table_names.?[0]); +} + +test "json_1_0_query_no_input: dynamodb listTables runtime" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"AccountMaxReadCapacityUnits":80000,"AccountMaxWriteCapacityUnits":80000,"TableMaxReadCapacityUnits":40000,"TableMaxWriteCapacityUnits":40000} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const dynamo_db = (Services(.{.dynamo_db}){}).dynamo_db; + const call = try test_harness.client.call(dynamo_db.describe_limits.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try test_harness.request_options.expectHeader("X-Amz-Target", "DynamoDB_20120810.DescribeLimits"); + try std.testing.expectEqualStrings( + \\{} + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG", call.response_metadata.request_id); + try std.testing.expectEqual(@as(i64, 80000), call.response.account_max_read_capacity_units.?); +} +test "json_1_1_query_with_input: ecs listClusters runtime" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"clusterArns":["arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster"],"nextToken":"czE0Og=="} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "b2420066-ff67-4237-b782-721c4df60744" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const ecs = (Services(.{.ecs}){}).ecs; + const call = try test_harness.client.call(ecs.list_clusters.Request{ + .max_results = 1, + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try test_harness.request_options.expectHeader("X-Amz-Target", "AmazonEC2ContainerServiceV20141113.ListClusters"); + + const parsed_body = try std.json.parseFromSlice(struct { + nextToken: ?[]const u8, + maxResults: u8, + }, testing.allocator, test_harness.request_options.request_body, .{}); + defer parsed_body.deinit(); + + try testing.expectEqual(null, parsed_body.value.nextToken); + try testing.expectEqual(1, parsed_body.value.maxResults); + + // Response expectations + try std.testing.expectEqualStrings("b2420066-ff67-4237-b782-721c4df60744", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 1), call.response.cluster_arns.?.len); + try std.testing.expectEqualStrings("arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster", call.response.cluster_arns.?[0]); +} +test "json_1_1_query_no_input: ecs listClusters runtime" { + // const old = std.testing.log_level; + // defer std.testing.log_level = old; + // std.testing.log_level = .debug; + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"clusterArns":["arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster"],"nextToken":"czE0Og=="} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "e65322b2-0065-45f2-ba37-f822bb5ce395" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const ecs = (Services(.{.ecs}){}).ecs; + const call = try test_harness.client.call(ecs.list_clusters.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try test_harness.request_options.expectHeader("X-Amz-Target", "AmazonEC2ContainerServiceV20141113.ListClusters"); + + const parsed_body = try std.json.parseFromSlice(struct { + nextToken: ?[]const u8, + maxResults: ?u8, + }, testing.allocator, test_harness.request_options.request_body, .{}); + defer parsed_body.deinit(); + + try testing.expectEqual(null, parsed_body.value.nextToken); + try testing.expectEqual(null, parsed_body.value.maxResults); + + // Response expectations + try std.testing.expectEqualStrings("e65322b2-0065-45f2-ba37-f822bb5ce395", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 1), call.response.cluster_arns.?.len); + try std.testing.expectEqualStrings("arn:aws:ecs:us-west-2:550620852718:cluster/web-applicationehjaf-cluster", call.response.cluster_arns.?[0]); +} +test "rest_json_1_query_with_input: lambda listFunctions runtime" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"Functions":[{"Description":"AWS CDK resource provider framework - onEvent (DevelopmentFrontendStack-g650u/com.amazonaws.cdk.custom-resources.amplify-asset-deployment-provider/amplify-asset-deployment-handler-provider)","TracingConfig":{"Mode":"PassThrough"},"VpcConfig":null,"SigningJobArn":null,"SnapStart":{"OptimizationStatus":"Off","ApplyOn":"None"},"RevisionId":"0c62fc74-a692-403d-9206-5fcbad406424","LastModified":"2023-03-01T18:13:15.704+0000","FileSystemConfigs":null,"FunctionName":"DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU","Runtime":"nodejs14.x","Version":"$LATEST","PackageType":"Zip","LastUpdateStatus":null,"Layers":null,"FunctionArn":"arn:aws:lambda:us-west-2:550620852718:function:DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU","KMSKeyArn":null,"MemorySize":128,"ImageConfigResponse":null,"LastUpdateStatusReason":null,"DeadLetterConfig":null,"Timeout":900,"Handler":"framework.onEvent","CodeSha256":"m4tt+M0l3p8bZvxIDj83dwGrwRW6atCfS/q8AiXCD3o=","Role":"arn:aws:iam::550620852718:role/DevelopmentFrontendStack-amplifyassetdeploymentha-1782JF7WAPXZ3","SigningProfileVersionArn":null,"MasterArn":null,"RuntimeVersionConfig":null,"CodeSize":4307,"State":null,"StateReason":null,"Environment":{"Variables":{"USER_ON_EVENT_FUNCTION_ARN":"arn:aws:lambda:us-west-2:550620852718:function:DevelopmentFrontendStack--amplifyassetdeploymenton-X9iZJSCSPYDH","WAITER_STATE_MACHINE_ARN":"arn:aws:states:us-west-2:550620852718:stateMachine:amplifyassetdeploymenthandlerproviderwaiterstatemachineB3C2FCBE-Ltggp5wBcHWO","USER_IS_COMPLETE_FUNCTION_ARN":"arn:aws:lambda:us-west-2:550620852718:function:DevelopmentFrontendStack--amplifyassetdeploymentis-jaHopLrSSARV"},"Error":null},"EphemeralStorage":{"Size":512},"StateReasonCode":null,"LastUpdateStatusReasonCode":null,"Architectures":["x86_64"]}],"NextMarker":"lslTXFcbLQKkb0vP9Kgh5hUL7C3VghELNGbWgZfxrRCk3eiDRMkct7D8EmptWfHSXssPdS7Bo66iQPTMpVOHZgANewpgGgFGGr4pVjd6VgLUO6qPe2EMAuNDBjUTxm8z6N28yhlUwEmKbrAV/m0k5qVzizwoxFwvyruMbuMx9kADFACSslcabxXl3/jDI4rfFnIsUVdzTLBgPF1hzwrE1f3lcdkBvUp+QgY+Pn3w5QuJmwsp/di8COzFemY89GgOHbLNqsrBsgR/ee2eXoJp0ZkKM4EcBK3HokqBzefLfgR02PnfNOdXwqTlhkSPW0TKiKGIYu3Bw7lSNrLd+q3+wEr7ZakqOQf0BVo3FMRhMHlVYgwUJzwi3ActyH2q6fuqGG1sS0B8Oa/prUpe5fmp3VaA3WpazioeHtrKF78JwCi6/nfQsrj/8ZtXGQOxlwEgvT1CIUaF+CdHY3biezrK0tRZNpkCtHnkPtF9lq2U7+UiKXSW9yzxT8P2b0M/Qh4IVdnw4rncQK/doYriAeOdrs1wjMEJnHWq9lAaEyipoxYcVr/z5+yaC6Gwxdg45p9X1vIAaYMf6IZxyFuua43SYi0Ls+IBk4VvpR2io7T0dCxHAr3WAo3D2dm0y8OsbM59"} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "c4025199-226f-4a16-bb1f-48618e9d2ea6" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const lambda = (Services(.{.lambda}){}).lambda; + const call = try test_harness.client.call(lambda.list_functions.Request{ + .max_items = 1, + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/2015-03-31/functions?MaxItems=1", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\ + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("c4025199-226f-4a16-bb1f-48618e9d2ea6", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 1), call.response.functions.?.len); + try std.testing.expectEqualStrings( + "DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU", + call.response.functions.?[0].function_name.?, + ); +} +test "rest_json_1_query_no_input: lambda listFunctions runtime" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = @embedFile("test_rest_json_1_query_no_input.response"), + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "b2aad11f-36fc-4d0d-ae92-fe0167fb0f40" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const lambda = (Services(.{.lambda}){}).lambda; + const call = try test_harness.client.call(lambda.list_functions.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/2015-03-31/functions", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\ + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("b2aad11f-36fc-4d0d-ae92-fe0167fb0f40", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 24), call.response.functions.?.len); + try std.testing.expectEqualStrings( + "DevelopmentFrontendStack--amplifyassetdeploymentha-aZqB9IbZLIKU", + call.response.functions.?[0].function_name.?, + ); + try std.testing.expectEqualStrings( + "amplify-login-create-auth-challenge-b4883e4c", + call.response.functions.?[12].function_name.?, + ); +} +test "rest_json_1_work_with_lambda: lambda tagResource (only), to excercise zig issue 17015" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = "", + .server_response_status = .no_content, + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "a521e152-6e32-4e67-9fb3-abc94e34551b" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const lambda = (Services(.{.lambda}){}).lambda; + var tags = try std.ArrayList(@typeInfo(try typeForField(lambda.tag_resource.Request, "tags")).pointer.child).initCapacity(allocator, 1); + defer tags.deinit(allocator); + tags.appendAssumeCapacity(.{ .key = "Foo", .value = "Bar" }); + const req = services.lambda.tag_resource.Request{ .resource = "arn:aws:lambda:us-west-2:550620852718:function:awsome-lambda-LambdaStackawsomeLambda", .tags = tags.items }; + const call = try aws.Request(lambda.tag_resource).call(req, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + + const parsed_body = try std.json.parseFromSlice(struct { + Tags: struct { + Foo: []const u8, + }, + }, testing.allocator, test_harness.request_options.request_body, .{ .ignore_unknown_fields = true }); + defer parsed_body.deinit(); + + try testing.expectEqualStrings("Bar", parsed_body.value.Tags.Foo); + + // Due to 17015, we see %253A instead of %3A + try std.testing.expectEqualStrings("/2017-03-31/tags/arn%3Aaws%3Alambda%3Aus-west-2%3A550620852718%3Afunction%3Aawsome-lambda-LambdaStackawsomeLambda", test_harness.request_options.request_target); + // Response expectations + try std.testing.expectEqualStrings("a521e152-6e32-4e67-9fb3-abc94e34551b", call.response_metadata.request_id); +} +test "rest_json_1_url_parameters_not_in_request: lambda update_function_code" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = "{\"CodeSize\": 42}", + .server_response_status = .ok, + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "a521e152-6e32-4e67-9fb3-abc94e34551b" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const lambda = (Services(.{.lambda}){}).lambda; + const architectures = [_][]const u8{"x86_64"}; + const arches: [][]const u8 = @constCast(architectures[0..]); + const req = services.lambda.update_function_code.Request{ + .function_name = "functionname", + .architectures = arches, + .zip_file = "zipfile", + }; + const call = try aws.Request(lambda.update_function_code).call(req, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); + + const parsed_body = try std.json.parseFromSlice(struct { + ZipFile: []const u8, + Architectures: [][]const u8, + }, testing.allocator, test_harness.request_options.request_body, .{ + .ignore_unknown_fields = true, + }); + defer parsed_body.deinit(); + + try testing.expectEqualStrings("zipfile", parsed_body.value.ZipFile); + try testing.expectEqual(1, parsed_body.value.Architectures.len); + try testing.expectEqualStrings("x86_64", parsed_body.value.Architectures[0]); + + // Due to 17015, we see %253A instead of %3A + try std.testing.expectEqualStrings("/2015-03-31/functions/functionname/code", test_harness.request_options.request_target); + // Response expectations + try std.testing.expectEqualStrings("a521e152-6e32-4e67-9fb3-abc94e34551b", call.response_metadata.request_id); +} +test "ec2_query_no_input: EC2 describe regions" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = @embedFile("test_ec2_query_no_input.response"), + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "text/xml;charset=UTF-8" }, + .{ .name = "x-amzn-RequestId", .value = "4cdbdd69-800c-49b5-8474-ae4c17709782" }, + }, + .server_response_transfer_encoding = .chunked, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const ec2 = (Services(.{.ec2}){}).ec2; + const call = try test_harness.client.call(ec2.describe_regions.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/?Action=DescribeRegions&Version=2016-11-15", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\Action=DescribeRegions&Version=2016-11-15 + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("4cdbdd69-800c-49b5-8474-ae4c17709782", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 17), call.response.regions.?.len); +} +// LLVM hates this test. Depending on the platform, it will consume all memory +// on the compilation host. Windows x86_64 and Linux riscv64 seem to be a problem so far +// riscv64-linux also seems to have another problem with LLVM basically infinitely +// doing something. My guess is the @embedFile is freaking out LLVM +test "ec2_query_with_input: EC2 describe instances" { + if (builtin.cpu.arch == .riscv64 and builtin.os.tag == .linux) return error.SkipZigTest; + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = @embedFile("test_ec2_query_with_input.response"), + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "text/xml;charset=UTF-8" }, + .{ .name = "x-amzn-RequestId", .value = "150a14cc-785d-476f-a4c9-2aa4d03b14e2" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const ec2 = (Services(.{.ec2}){}).ec2; + const call = try test_harness.client.call(ec2.describe_instances.Request{ + .max_results = 6, + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/?Action=DescribeInstances&Version=2016-11-15", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\Action=DescribeInstances&Version=2016-11-15&MaxResults=6 + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("150a14cc-785d-476f-a4c9-2aa4d03b14e2", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 6), call.response.reservations.?.len); + try std.testing.expectEqualStrings("i-0212d7d1f62b96676", call.response.reservations.?[1].instances.?[0].instance_id.?); + try std.testing.expectEqualStrings("123456789012:found-me", call.response.reservations.?[1].instances.?[0].tags.?[0].value.?); +} +test "rest_xml_with_input_s3: S3 create bucket" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\ + , + .server_response_headers = &.{ // I don't see content type coming back in actual S3 requests + .{ .name = "x-amzn-RequestId", .value = "9PEYBAZ9J7TPRX43" }, + .{ .name = "x-amz-id-2", .value = "u7lzgW0tIyRP15vSUsVOXxJ37OfVCO8lZmLIVuqeq5EE4tNp9qebb5fy+/kendlZpR4YQE+y4Xg=" }, + }, + }); + defer test_harness.deinit(); + errdefer test_harness.creds.deinit(); + const options = try test_harness.start(); + const s3 = (Services(.{.s3}){}).s3; + const call = try test_harness.client.call(s3.create_bucket.Request{ + .bucket = "", + .create_bucket_configuration = .{ + .location_constraint = "us-west-2", + }, + }, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try std.testing.expectEqualStrings( + \\ + \\ us-west-2 + \\ + , test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings( + "9PEYBAZ9J7TPRX43, host_id: u7lzgW0tIyRP15vSUsVOXxJ37OfVCO8lZmLIVuqeq5EE4tNp9qebb5fy+/kendlZpR4YQE+y4Xg=", + call.response_metadata.request_id, + ); +} +test "rest_xml_no_input: S3 list buckets" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\3367189aa775bd98da38e55093705f2051443c1e775fc0971d6d77387a47c8d0emilerch+sub1550620852718-backup2020-06-17T16:26:51.000Zamplify-letmework-staging-185741-deployment2023-03-10T18:57:49.000Zaws-cloudtrail-logs-550620852718-224022a72021-06-21T18:32:44.000Zaws-sam-cli-managed-default-samclisourcebucket-1gy0z00mj47xe2021-10-05T16:38:07.000Zawsomeprojectstack-pipelineartifactsbucketaea9a05-1uzwo6c86ecr2021-10-05T22:55:09.000Zcdk-hnb659fds-assets-550620852718-us-west-22023-02-28T21:49:36.000Zcf-templates-12iy6putgdxtk-us-west-22020-06-26T02:31:59.000Zcodepipeline-us-west-2-467140836372021-09-14T18:43:07.000Zelasticbeanstalk-us-west-2-5506208527182022-04-15T16:22:42.000Zlobo-west2021-06-21T17:17:22.000Zlobo-west-22021-11-19T20:12:31.000Zlogging-backup-550620852718-us-east-22022-05-29T21:55:16.000Zmysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr02023-03-01T04:53:55.000Z + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/xml" }, + .{ .name = "x-amzn-RequestId", .value = "9PEYBAZ9J7TPRX43" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const s3 = (Services(.{.s3}){}).s3; + const call = try test_harness.client.call(s3.list_buckets.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); + // This changed in rev 830202d722c904c7e3da40e8dde7b9338d08752c of the go sdk, and + // contrary to the documentation, a query string argument was added. My guess is that + // there is no functional reason, and that this is strictly for some AWS reporting function. + // Alternatively, it could be to support some customization mechanism, as the commit + // title of that commit is "Merge customizations for S3" + try std.testing.expectEqualStrings("/?x-id=ListBuckets", test_harness.request_options.request_target); + try std.testing.expectEqualStrings("", test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("9PEYBAZ9J7TPRX43", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 13), call.response.buckets.?.len); +} +test "rest_xml_anything_but_s3: CloudFront list key groups" { + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"Items":null,"MaxItems":100,"NextMarker":null,"Quantity":0} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "d3382082-5291-47a9-876b-8df3accbb7ea" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const cloudfront = (Services(.{.cloudfront}){}).cloudfront; + const call = try test_harness.client.call(cloudfront.list_key_groups.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.GET, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/2020-05-31/key-group", test_harness.request_options.request_target); + try std.testing.expectEqualStrings("", test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("d3382082-5291-47a9-876b-8df3accbb7ea", call.response_metadata.request_id); + try std.testing.expectEqual(@as(i64, 100), call.response.key_group_list.?.max_items); +} +test "rest_xml_with_input: S3 put object" { + // const old = std.testing.log_level; + // defer std.testing.log_level = old; + // std.testing.log_level = .debug; + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = "", + .server_response_headers = &.{ + // .{ "Content-Type", "application/xml" }, + .{ .name = "x-amzn-RequestId", .value = "9PEYBAZ9J7TPRX43" }, + .{ .name = "x-amz-id-2", .value = "jdRDo30t7Ge9lf6F+4WYpg+YKui8z0mz2+rwinL38xDZzvloJqrmpCAiKG375OSvHA9OBykJS44=" }, + .{ .name = "x-amz-server-side-encryption", .value = "AES256" }, + .{ .name = "ETag", .value = "37b51d194a7513e45b56f6524f2d51f2" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const s3opts = aws.Options{ + .region = "us-west-2", + .client = options.client, + .signing_time = TestSetup.signing_time, + }; + const result = try aws.Request(services.s3.put_object).call(.{ + .bucket = "mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0", + .key = "i/am/a/teapot/foo", + .content_type = "text/plain", + .body = "bar", + .storage_class = "STANDARD", + }, s3opts); + defer result.deinit(); + for (test_harness.request_options.request_headers) |header| { + std.log.info("Request header: {s}: {s}", .{ header.name, header.value }); + } + try test_harness.request_options.expectNoDuplicateHeaders(); + std.log.info("PutObject Request id: {s}", .{result.response_metadata.request_id}); + std.log.info("PutObject etag: {s}", .{result.response.e_tag.?}); + //mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0.s3.us-west-2.amazonaws.com + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.PUT, test_harness.request_options.request_method); + // I don't think this will work since we're overriding the url + // try test_harness.request_options.expectHeader("Host", "mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0.s3.us-west-2.amazonaws.com"); + try test_harness.request_options.expectHeader("x-amz-storage-class", "STANDARD"); + try std.testing.expectEqualStrings("/mysfitszj3t6webstack-hostingbucketa91a61fe-1ep3ezkgwpxr0/i/am/a/teapot/foo?x-id=PutObject", test_harness.request_options.request_target); + try std.testing.expectEqualStrings("bar", test_harness.request_options.request_body); + // Response expectations + try std.testing.expectEqualStrings("9PEYBAZ9J7TPRX43, host_id: jdRDo30t7Ge9lf6F+4WYpg+YKui8z0mz2+rwinL38xDZzvloJqrmpCAiKG375OSvHA9OBykJS44=", result.response_metadata.request_id); + try std.testing.expectEqualStrings("AES256", result.response.server_side_encryption.?); + try std.testing.expectEqualStrings("37b51d194a7513e45b56f6524f2d51f2", result.response.e_tag.?); +} +test "raw ECR timestamps" { + // This is a way to test the json parsing. Ultimately the more robust tests + // should be preferred, but in this case we were tracking down an issue + // for which the root cause was the incorrect type being passed to the parse + // routine + const allocator = std.testing.allocator; + const ecr = (Services(.{.ecr}){}).ecr; + const options = json.ParseOptions{ + .allocator = allocator, + .allow_camel_case_conversion = true, // new option + .allow_snake_case_conversion = true, // new option + .allow_unknown_fields = true, // new option. Cannot yet handle non-struct fields though + .allow_missing_fields = false, // new option. Cannot yet handle non-struct fields though + }; + var stream = json.TokenStream.init( + \\{"authorizationData":[{"authorizationToken":"***","expiresAt":1.7385984915E9,"proxyEndpoint":"https://146325435496.dkr.ecr.us-west-2.amazonaws.com"}]} + ); + const ptr = try json.parse(ecr.get_authorization_token.Response, &stream, options); + defer json.parseFree(ecr.get_authorization_token.Response, ptr, options); +} +test "json_1_1: ECR timestamps" { + // See: https://github.com/elerch/aws-sdk-for-zig/issues/5 + // const old = std.testing.log_level; + // defer std.testing.log_level = old; + // std.testing.log_level = .debug; + const allocator = std.testing.allocator; + + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{"authorizationData":[{"authorizationToken":"***","expiresAt":"2022-05-17T06:56:13.652000+00:00","proxyEndpoint":"https://146325435496.dkr.ecr.us-west-2.amazonaws.com"}]} + // \\{"authorizationData":[{"authorizationToken":"***","expiresAt":1.738598491557E9,"proxyEndpoint":"https://146325435496.dkr.ecr.us-west-2.amazonaws.com"}]} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, + }, + }); + defer test_harness.deinit(); + const options = try test_harness.start(); + const ecr = (Services(.{.ecr}){}).ecr; + std.log.debug("Typeof response {}", .{@TypeOf(ecr.get_authorization_token.Response{})}); + const call = try test_harness.client.call(ecr.get_authorization_token.Request{}, options); + defer call.deinit(); + test_harness.stop(); + // Request expectations + try std.testing.expectEqual(std.http.Method.POST, test_harness.request_options.request_method); + try std.testing.expectEqualStrings("/", test_harness.request_options.request_target); + try test_harness.request_options.expectHeader("X-Amz-Target", "AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken"); + // Response expectations + try std.testing.expectEqualStrings("QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG", call.response_metadata.request_id); + try std.testing.expectEqual(@as(usize, 1), call.response.authorization_data.?.len); + try std.testing.expectEqualStrings("***", call.response.authorization_data.?[0].authorization_token.?); + try std.testing.expectEqualStrings("https://146325435496.dkr.ecr.us-west-2.amazonaws.com", call.response.authorization_data.?[0].proxy_endpoint.?); + // try std.testing.expectEqual(@as(i64, 1.73859841557E9), call.response.authorization_data.?[0].expires_at.?); + + const zeit = @import("zeit"); + const expected_ins = try zeit.instant(.{ + .source = .{ .iso8601 = "2022-05-17T06:56:13.652000+00:00" }, + }); + const expected_ts: date.Timestamp = @enumFromInt(expected_ins.timestamp); + + try std.testing.expectEqual(expected_ts, call.response.authorization_data.?[0].expires_at.?); +} +var test_error_log_enabled = true; +test "test server timeout works" { + // const old = std.testing.log_level; + // defer std.testing.log_level = old; + // std.testing.log_level = .debug; + // defer std.testing.log_level = old; + // std.testing.log_level = .debug; + test_error_log_enabled = false; + defer test_error_log_enabled = true; + std.log.debug("test start", .{}); + const allocator = std.testing.allocator; + var test_harness = TestSetup.init(.{ + .allocator = allocator, + .server_response = + \\{} + , + .server_response_headers = &.{ + .{ .name = "Content-Type", .value = "application/json" }, + .{ .name = "x-amzn-RequestId", .value = "QBI72OUIN8U9M9AG6PCSADJL4JVV4KQNSO5AEMVJF66Q9ASUAAJG" }, + }, + }); + defer test_harness.deinit(); + defer test_harness.creds.deinit(); // Usually this gets done during the call, + // but we're purposely not making a call + // here, so we have to deinit() manually + _ = try test_harness.start(); + std.log.debug("harness started", .{}); + test_harness.stop(); + std.log.debug("test complete", .{}); +} + +const testing = std.testing; + +test "jsonStringify: structure + enums" { + const request = services.media_convert.PutPolicyRequest{ + .policy = .{ + .http_inputs = "foo", + .https_inputs = "bar", + .s3_inputs = "baz", + }, + }; + + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); + defer std.testing.allocator.free(request_json); + + const parsed = try std.json.parseFromSlice(struct { + policy: struct { + httpInputs: []const u8, + httpsInputs: []const u8, + s3Inputs: []const u8, + }, + }, testing.allocator, request_json, .{}); + defer parsed.deinit(); + + try testing.expectEqualStrings("foo", parsed.value.policy.httpInputs); + try testing.expectEqualStrings("bar", parsed.value.policy.httpsInputs); + try testing.expectEqualStrings("baz", parsed.value.policy.s3Inputs); +} + +test "jsonStringify: strings" { + const request = services.media_convert.AssociateCertificateRequest{ + .arn = "1234", + }; + + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); + defer std.testing.allocator.free(request_json); + + try testing.expectEqualStrings("{\"arn\":\"1234\"}", request_json); +} + +test "jsonStringify" { + var tags = [_]services.media_convert.MapOfStringKeyValue{ + .{ + .key = "foo", + .value = "bar", + }, + }; + + const request = services.media_convert.TagResourceRequest{ + .arn = "1234", + .tags = &tags, + }; + + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); + defer std.testing.allocator.free(request_json); + + const json_parsed = try std.json.parseFromSlice(struct { + arn: []const u8, + tags: struct { + foo: []const u8, + }, + }, testing.allocator, request_json, .{}); + defer json_parsed.deinit(); + + try testing.expectEqualStrings("1234", json_parsed.value.arn); + try testing.expectEqualStrings("bar", json_parsed.value.tags.foo); +} + +test "jsonStringify nullable object" { + // structure is not null + { + const request = services.lambda.CreateAliasRequest{ + .function_name = "foo", + .function_version = "bar", + .name = "baz", + .routing_config = services.lambda.AliasRoutingConfiguration{ + .additional_version_weights = null, + }, + }; + + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); + defer std.testing.allocator.free(request_json); + + const json_parsed = try std.json.parseFromSlice(struct { + FunctionName: []const u8, + FunctionVersion: []const u8, + Name: []const u8, + RoutingConfig: struct { + AdditionalVersionWeights: ?struct {}, + }, + }, testing.allocator, request_json, .{ .ignore_unknown_fields = true }); + defer json_parsed.deinit(); + + try testing.expectEqualStrings("foo", json_parsed.value.FunctionName); + try testing.expectEqualStrings("bar", json_parsed.value.FunctionVersion); + try testing.expectEqualStrings("baz", json_parsed.value.Name); + try testing.expectEqual(null, json_parsed.value.RoutingConfig.AdditionalVersionWeights); + } + + // structure is null + { + const request = services.kms.DecryptRequest{ + .key_id = "foo", + .ciphertext_blob = "bar", + }; + + const request_json = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{std.json.fmt(request, .{})}); + defer std.testing.allocator.free(request_json); + + const json_parsed = try std.json.parseFromSlice(struct { + KeyId: []const u8, + CiphertextBlob: []const u8, + }, testing.allocator, request_json, .{ .ignore_unknown_fields = true }); + defer json_parsed.deinit(); + + try testing.expectEqualStrings("foo", json_parsed.value.KeyId); + try testing.expectEqualStrings("bar", json_parsed.value.CiphertextBlob); + } +}