Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
2.0.0
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import Deferred
@testable import Sync
import XCTest
import SwiftyJSON
// Always return a gigantic encoded payload.
private func massivify<T>(_ record: Record<T>) -> JSON? {
return JSON([
"id": record.id,
"foo": String(repeating: "X", count: Sync15StorageClient.maxRecordSizeBytes + 1)
])
}
private func basicSerializer<T>(record: Record<T>) -> String {
return JSON([
"id": record.id,
"payload": record.payload.json.dictionaryObject as Any
]).stringValue()!
}
// Create a basic record with an ID and a title that is the `Site$ID`.
private func createRecordWithID(id: String) -> Record<CleartextPayloadJSON> {
let jsonString = "{\"id\":\"\(id)\",\"title\": \"\(id)\"}"
return Record<CleartextPayloadJSON>(id: id,
payload: CleartextPayloadJSON(JSON(parseJSON: jsonString)),
modified: 10_000,
sortindex: 123,
ttl: 1_000_000)
}
private func assertLinesMatchRecords<T>(lines: [String], records: [Record<T>], serializer: (Record<T>) -> String) {
guard lines.count == records.count else {
XCTFail("Number of lines mismatch number of records")
return
}
lines.enumerated().forEach { index, line in
let record = records[index]
XCTAssertEqual(line, serializer(record))
}
}
private func deferEmptyResponse(token batchToken: BatchToken? = nil, lastModified: Timestamp? = nil) -> Deferred<Maybe<StorageResponse<POSTResult>>> {
var headers = [String: Any]()
if let lastModified = lastModified {
headers["X-Last-Modified"] = lastModified
}
return deferMaybe(
StorageResponse(value: POSTResult(success: [], failed: [:], batchToken: batchToken), metadata: ResponseMetadata(status: 200, headers: headers))
)
}
// Small helper operator for comparing query parameters below
private func ==(param1: NSURLQueryItem, param2: NSURLQueryItem) -> Bool {
return param1.name == param2.name && param1.value == param2.value
}
private let miniConfig = InfoConfiguration(maxRequestBytes: 1_048_576, maxPostRecords: 2, maxPostBytes: 1_048_576, maxTotalRecords: 10, maxTotalBytes: 104_857_600)
class Sync15BatchClientTests: XCTestCase {
func testAddLargeRecordFails() {
let uploader: BatchUploadFunction = { _,_,_ in deferEmptyResponse(lastModified: 10_000) }
let serializeRecord = { massivify($0)?.stringValue() }
let batch = Sync15BatchClient(config: miniConfig,
ifUnmodifiedSince: nil,
serializeRecord: serializeRecord,
uploader: uploader,
onCollectionUploaded: { _,_ in deferMaybe(Date() as! MaybeErrorType)})
let record = createRecordWithID(id: "A")
let result = batch.addRecords([record]).value
XCTAssertTrue(result.isFailure)
XCTAssertTrue(result.failureValue! is RecordTooLargeError)
}
func testFailToSerializeRecord() {
let uploader: BatchUploadFunction = { _,_,_ in deferEmptyResponse(lastModified: 10_000) }
let batch = Sync15BatchClient(config: miniConfig,
ifUnmodifiedSince: nil,
serializeRecord: { _ in nil },
uploader: uploader,
onCollectionUploaded: { _,_ in deferMaybe(Date.now())})
let record = createRecordWithID(id: "A")
let result = batch.addRecords([record]).value
XCTAssertTrue(result.isFailure)
XCTAssertTrue(result.failureValue! is SerializeRecordFailure)
}
func testBackoffDuringBatchUploading() {
let uploader: BatchUploadFunction = { lines, ius, queryParams in
deferMaybe(ServerInBackoffError(until: 10_000))
}
// Setup a configuration so each batch supports two payloads of two records each
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 1,
maxPostBytes: 1_048_576,
maxTotalRecords: 4,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: { _,_ in deferMaybe(Date.now())})
let record = createRecordWithID(id: "A")
batch.addRecords([record]).succeeded()
let result = batch.endBatch().value
// Verify that when the server goes into backoff, we get those bubbled up through the batching client
XCTAssertTrue(result.isFailure)
XCTAssertTrue(result.failureValue! is ServerInBackoffError)
}
func testIfUnmodifiedSinceUpdatesForSinglePOSTs() {
var requestCount = 0
var linesSent = [String]()
var lastIUS: Timestamp? = 0
// Since we never return a batch token, the batch client won't batch upload and default to single POSTs
let uploader: BatchUploadFunction = { lines, ius, _ in
linesSent += lines
requestCount += 1
switch requestCount {
case 1:
XCTAssertEqual(ius, 10_000_000)
return deferEmptyResponse(lastModified: 20_000)
case 2:
XCTAssertEqual(ius, 20_000_000)
return deferEmptyResponse(lastModified: 30_000)
case 3:
XCTAssertEqual(ius, 30_000_000)
lastIUS = ius
return deferEmptyResponse(lastModified: 30_000)
default:
XCTFail()
return deferEmptyResponse(lastModified: 0)
}
}
let singleRecordConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 1,
maxPostBytes: 1_048_576,
maxTotalRecords: 10,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: singleRecordConfig,
ifUnmodifiedSince: 10_000_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: { _,_ in deferMaybe(Date.now())})
let recordA = createRecordWithID(id: "A")
let recordB = createRecordWithID(id: "B")
let recordC = createRecordWithID(id: "C")
let allRecords = [recordA, recordB, recordC]
batch.addRecords([recordA, recordB, recordC]).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent
XCTAssertEqual(requestCount, 3)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer)
// Validate the last IUS we got is the last request
XCTAssertEqual(lastIUS, 30_000_000)
}
func testUploadBatchUnsupportedBatching() {
var requestCount = 0
var uploadedCollectionCount = 0
var linesSent = [String]()
// Since we never return a batch token, the batch client won't batch upload and default to single POSTs
let uploader: BatchUploadFunction = { lines, ius, _ in
linesSent += lines
requestCount += 1
return deferEmptyResponse(lastModified: 10_000)
}
let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _,_ in
uploadedCollectionCount += 1
return deferMaybe(Date.now())
}
// Setup a configuration so we each payload would be one record
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 1,
maxPostBytes: 1_048_576,
maxTotalRecords: 10,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: collectionUploaded)
let recordA = createRecordWithID(id: "A")
let recordB = createRecordWithID(id: "B")
let allRecords = [recordA, recordB]
batch.addRecords([recordA, recordB]).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent. One for the start post, and one for the committing
XCTAssertEqual(requestCount, 2)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer)
// Validate we only made 2 calls to collection uploaded since we're doing single POSTs
XCTAssertEqual(uploadedCollectionCount, 2)
}
/**
Tests sending a batch consisting of 3 full payloads. This batch is regular in the sense that it should
contain a batch=true call, an upload within the batch, and finish with a commit=true upload.
*/
func testUploadRegularSingleBatch() {
var requestCount = 0
var uploadedCollectionCount = 0
var linesSent = [String]()
let allRecords: [Record<CleartextPayloadJSON>] = "ABCDEF".reduce([]) { list, char in
return list + [createRecordWithID(id: String(char))]
}
// Since we never return a batch token, the batch client won't batch upload and default to single POSTs
let uploader: BatchUploadFunction = { lines, ius, queryParams in
linesSent += lines
requestCount += 1
switch requestCount {
case 1:
let expected = URLQueryItem(name: "batch", value: "true")
XCTAssertEqual(expected, queryParams![0])
XCTAssertEqual(ius, 10_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer)
return deferEmptyResponse(token: "1", lastModified: 10_000)
case 2:
let expected = URLQueryItem(name: "batch", value: "1")
XCTAssertEqual(expected, queryParams![0])
XCTAssertEqual(ius, 10_000_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<4]), serializer: basicSerializer)
return deferEmptyResponse(token: "1", lastModified: 10_000)
case 3:
let expectedBatch = URLQueryItem(name: "batch", value: "1")
let expectedCommit = URLQueryItem(name: "commit", value: "true")
XCTAssertEqual(expectedBatch, queryParams![0])
XCTAssertEqual(expectedCommit, queryParams![1])
XCTAssertEqual(ius, 10_000_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[4..<6]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 20_000)
default:
XCTFail()
return deferEmptyResponse(lastModified: 0)
}
}
let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = {_,_ in
uploadedCollectionCount += 1
return deferMaybe(Date.now())
}
// Setup a configuration so we send 2 records per each payload
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 2,
maxPostBytes: 1_048_576,
maxTotalRecords: 10,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: collectionUploaded)
batch.addRecords(allRecords).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent. One for the start post, and one for the committing
XCTAssertEqual(requestCount, 3)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer)
// Validate we only made one call to the collection upload callback
XCTAssertEqual(uploadedCollectionCount, 2)
XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000)
}
/**
Tests pushing a batch where one of the payloads is not at limit.
*/
func testBatchUploadWithPartialPayload() {
var requestCount = 0
var uploadedCollectionCount = 0
var linesSent = [String]()
let allRecords: [Record<CleartextPayloadJSON>] = "ABC".reduce([]) { list, char in
return list + [createRecordWithID(id: String(char))]
}
// Since we never return a batch token, the batch client won't batch upload and default to single POSTs
let uploader: BatchUploadFunction = { lines, ius, queryParams in
linesSent += lines
requestCount += 1
switch requestCount {
case 1:
let expected = URLQueryItem(name: "batch", value: "true")
XCTAssertEqual(expected, queryParams![0])
XCTAssertEqual(ius, 10_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer)
return deferEmptyResponse(token: "1", lastModified: 10_000)
case 2:
let expectedBatch = URLQueryItem(name: "batch", value: "1")
let expectedCommit = URLQueryItem(name: "commit", value: "true")
XCTAssertEqual(expectedBatch, queryParams![0])
XCTAssertEqual(expectedCommit, queryParams![1])
XCTAssertEqual(ius, 10_000_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<3]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 20_000)
default:
XCTFail()
return deferEmptyResponse(lastModified: 0)
}
}
let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = {_,_ in
uploadedCollectionCount += 1
return deferMaybe(Date.now())
}
// Setup a configuration so we send 2 records per each payload
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 2,
maxPostBytes: 1_048_576,
maxTotalRecords: 10,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: collectionUploaded)
batch.addRecords(allRecords).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent. One for the start post, and one for the committing
XCTAssertEqual(requestCount, 2)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer)
// Validate we only made one call to the collection upload callback
XCTAssertEqual(uploadedCollectionCount, 2)
XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000)
}
/**
Attempt to send 3 payloads: 2 which are full, 1 that is partial, within a batch that only supports 5 records.
*/
func testBatchUploadWithUnevenPayloadsInBatch() {
var requestCount = 0
var uploadedCollectionCount = 0
var linesSent = [String]()
let allRecords: [Record<CleartextPayloadJSON>] = "ABCDE".reduce([]) { list, char in
return list + [createRecordWithID(id: String(char))]
}
// Since we never return a batch token, the batch client won't batch upload and default to single POSTs
let uploader: BatchUploadFunction = { lines, ius, queryParams in
linesSent += lines
requestCount += 1
switch requestCount {
case 1:
let expected = URLQueryItem(name: "batch", value: "true")
XCTAssertEqual(expected, queryParams![0])
XCTAssertEqual(ius, 10_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer)
return deferEmptyResponse(token: "1", lastModified: 10_000)
case 2:
let expectedBatch = URLQueryItem(name: "batch", value: "1")
XCTAssertEqual(expectedBatch, queryParams![0])
XCTAssertEqual(ius, 10_000_000)
assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<4]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 10_000)
case 3:
let expectedBatch = URLQueryItem(name: "batch", value: "1")
let expectedCommit = URLQueryItem(name: "commit", value: "true")
XCTAssertEqual(expectedBatch, queryParams![0])
XCTAssertEqual(expectedCommit, queryParams![1])
XCTAssertEqual(ius, 10_000_000)
assertLinesMatchRecords(lines: lines, records: [allRecords[4]], serializer: basicSerializer)
return deferEmptyResponse(lastModified: 20_000)
default:
XCTFail()
return deferEmptyResponse(lastModified: 0)
}
}
let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = {_,_ in
uploadedCollectionCount += 1
return deferMaybe(Date.now())
}
// Setup a configuration so we send 2 records per each payload
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 2,
maxPostBytes: 1_048_576,
maxTotalRecords: 5,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: collectionUploaded)
batch.addRecords(allRecords).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent. One for the start post, and one for the committing
XCTAssertEqual(requestCount, 3)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer)
// Validate we only made one call to the collection upload callback
XCTAssertEqual(uploadedCollectionCount, 2)
XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000)
}
/**
Tests pushing up a single payload as part of a batch.
*/
func testBatchUploadWithSinglePayload() {
var requestCount = 0
var uploadedCollectionCount = 0
var linesSent = [String]()
let recordA = createRecordWithID(id: "A")
// Since we never return a batch token, the batch client won't batch upload and default to single POSTs
let uploader: BatchUploadFunction = { lines, ius, queryParams in
linesSent += lines
requestCount += 1
switch requestCount {
case 1:
XCTAssertEqual(ius, 10_000)
assertLinesMatchRecords(lines: lines, records: [recordA], serializer: basicSerializer)
return deferEmptyResponse(lastModified: 20_000)
default:
XCTFail()
return deferEmptyResponse(lastModified: 0)
}
}
let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = {_,_ in
uploadedCollectionCount += 1
return deferMaybe(Date.now())
}
// Setup a configuration so we send 2 records per each payload
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 2,
maxPostBytes: 1_048_576,
maxTotalRecords: 10,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: collectionUploaded)
batch.addRecords([recordA]).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent. One for the start post, and one for the committing
XCTAssertEqual(requestCount, 1)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: [recordA], serializer: basicSerializer)
// Validate we only made one call to the collection upload callback
XCTAssertEqual(uploadedCollectionCount, 1)
XCTAssertEqual(batch.ifUnmodifiedSince!, 20_000_000)
}
func testMultipleBatchUpload() {
var requestCount = 0
var uploadedCollectionCount = 0
var linesSent = [String]()
let allRecords: [Record<CleartextPayloadJSON>] = "ABCDEFGHIJKL".reduce([]) { list, char in
return list + [createRecordWithID(id: String(char))]
}
// For each upload, verify that we are getting the correct queryParams and records to be sent.
let uploader: BatchUploadFunction = { lines, ius, queryParams in
linesSent += lines
requestCount += 1
switch requestCount {
case 1:
let expected = URLQueryItem(name: "batch", value: "true")
XCTAssertEqual(expected, queryParams![0])
assertLinesMatchRecords(lines: lines, records: Array(allRecords[0..<2]), serializer: basicSerializer)
return deferEmptyResponse(token: "1", lastModified: 20_000)
case 2:
let expectedBatch = URLQueryItem(name: "batch", value: "1")
XCTAssertEqual(expectedBatch, queryParams![0])
assertLinesMatchRecords(lines: lines, records: Array(allRecords[2..<4]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 20_000)
case 3:
let expectedBatch = URLQueryItem(name: "batch", value: "1")
let expectedCommit = URLQueryItem(name: "commit", value: "true")
XCTAssertEqual(expectedBatch, queryParams![0])
XCTAssertEqual(expectedCommit, queryParams![1])
assertLinesMatchRecords(lines: lines, records: Array(allRecords[4..<6]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 20_000)
case 4:
let expected = URLQueryItem(name: "batch", value: "true")
XCTAssertEqual(expected, queryParams![0])
assertLinesMatchRecords(lines: lines, records: Array(allRecords[6..<8]), serializer: basicSerializer)
return deferEmptyResponse(token: "2", lastModified: 30_000)
case 5:
let expectedBatch = URLQueryItem(name: "batch", value: "2")
XCTAssertEqual(expectedBatch, queryParams![0])
assertLinesMatchRecords(lines: lines, records: Array(allRecords[8..<10]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 30_000)
case 6:
let expectedBatch = URLQueryItem(name: "batch", value: "2")
let expectedCommit = URLQueryItem(name: "commit", value: "true")
XCTAssertEqual(expectedBatch, queryParams![0])
XCTAssertEqual(expectedCommit, queryParams![1])
assertLinesMatchRecords(lines: lines, records: Array(allRecords[10..<12]), serializer: basicSerializer)
return deferEmptyResponse(lastModified: 30_000)
default:
XCTFail()
return deferEmptyResponse(lastModified: 0)
}
}
let collectionUploaded: (POSTResult, Timestamp?) -> DeferredTimestamp = { _,_ in
uploadedCollectionCount += 1
return deferMaybe(Date.now())
}
// Setup a configuration so each batch supports two payloads of two records each
let twoRecordBatchesConfig = InfoConfiguration(
maxRequestBytes: 1_048_576,
maxPostRecords: 2,
maxPostBytes: 1_048_576,
maxTotalRecords: 6,
maxTotalBytes: 104_857_600
)
let batch = Sync15BatchClient(config: twoRecordBatchesConfig,
ifUnmodifiedSince: 10_000_000,
serializeRecord: basicSerializer,
uploader: uploader,
onCollectionUploaded: collectionUploaded)
batch.addRecords(allRecords).succeeded()
batch.endBatch().succeeded()
// Validate number of requests sent. One for the start post, and one for the committing
XCTAssertEqual(requestCount, 6)
// Validate contents sent to the server
assertLinesMatchRecords(lines: linesSent, records: allRecords, serializer: basicSerializer)
// Validate we only called collection uploaded when we start and finish a batch. The uploads inside
// a batch should not trigger the callback.
XCTAssertEqual(uploadedCollectionCount, 4)
}
}