Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Core/CommonKit/Sources/CommonKit/Models/PartialMockModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// PartialMockModel.swift
//
//
// Created for Trendyol Marketing Object Testing
//

import Foundation
import AnyCodable

public struct PartialMockModel: Codable {
public let deviceId: String
public let url: String
public let method: String
public let mockDomain: String
public let modifications: [PartialMockModification]
}

public struct PartialMockModification: Codable {
public let path: String
public let operations: [PartialMockOperation]
}

/// A single operation to apply to a matched JSON node's child member.
/// Operation types and semantics follow RFC 6902 (JSON Patch).
///
/// - `key`: The target child member name (corresponds to the last segment of the RFC 6902 `path`).
/// - `value`: Required for `add`, `replace`, `test`. Ignored for `remove`, `move`, `copy`.
/// - `from`: Required for `move`, `copy`. The source child member name (corresponds to RFC 6902 `from`).
public struct PartialMockOperation: Codable {
public let type: PartialMockOperationType
public let key: String
public let value: AnyCodableModel?
public let from: String?
}

/// RFC 6902 JSON Patch operation types.
/// https://datatracker.ietf.org/doc/html/rfc6902#section-4
public enum PartialMockOperationType: String, Codable {
/// Adds a member to the target object. If the member already exists, its value is replaced.
case add
/// Removes the member from the target object. The member MUST exist.
case remove
/// Replaces the value of the target member. The member MUST exist.
case replace
/// Removes the value at `from` and adds it to the target location.
case move
/// Copies the value at `from` to the target location.
case copy
/// Tests that the value at the target location equals the specified value.
case test
}
9 changes: 9 additions & 0 deletions Core/MockingStarCore/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Core/MockingStarCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ let package = Package(
.package(path: "../Server"),
.package(path: "../CommonKit"),
.package(path: "../PluginCore"),
.package(url: "https://github.com/objecthub/swift-dynamicjson", from: "1.0.0"),
],
targets: [
.target(
name: "MockingStarCore",
dependencies: [
"Server",
"CommonKit",
.product(name: "DynamicJSON", package: "swift-dynamicjson"),
.product(name: "PluginCore", package: "PluginCore", condition: .when(platforms: [.macOS])),
.product(name: "PluginCoreLinux", package: "PluginCore", condition: .when(platforms: [.linux])),
]
Expand Down
50 changes: 45 additions & 5 deletions Core/MockingStarCore/Sources/MockingStarCore/MockingStar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public protocol MockingStarCoreInterface {
public final class MockingStarCore {
private let deciderActor = MockDeciderActor()
private let scenariosActor = ScenarioDecidersActor()
private let partialMocksActor = PartialMockDecidersActor()
private let saverActor = FileSaverActor()
private let jsonEncoder = JSONEncoder.shared
private let jsonDecoder = JSONDecoder.shared
Expand All @@ -48,7 +49,8 @@ public final class MockingStarCore {

try await Task.sleep(for: .seconds(mock.metaData.responseTime))

let bodyData = mock.responseBody.data(using: .utf8) ?? .init()
var bodyData = mock.responseBody.data(using: .utf8) ?? .init()
bodyData = await applyPartialMocks(to: bodyData, request: request, flags: flags)
return (status: mock.metaData.httpStatus,
body: bodyData,
headers: try mock.responseHeader.asDictionary())
Expand All @@ -61,7 +63,8 @@ public final class MockingStarCore {
"traceUrl": .string(request.url?.absoluteString ?? "")
])

let liveResult = try await proxyRequest(request: request, mockDomain: flags.domain)
var liveResult = try await proxyRequest(request: request, mockDomain: flags.domain)
liveResult.body = await applyPartialMocks(to: liveResult.body, request: request, flags: flags)
Task {
try await saveFileIfNeeded(request: request,
flags: flags,
Expand Down Expand Up @@ -90,7 +93,8 @@ public final class MockingStarCore {
"traceUrl": .string(request.url?.absoluteString ?? "")
])

let liveResult = try await proxyRequest(request: request, mockDomain: flags.domain)
var liveResult = try await proxyRequest(request: request, mockDomain: flags.domain)
liveResult.body = await applyPartialMocks(to: liveResult.body, request: request, flags: flags)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we save partial modified mock, or save as original?

Task {
try await saveFileIfNeeded(request: request,
flags: flags,
Expand Down Expand Up @@ -178,7 +182,7 @@ public final class MockingStarCore {
logger.debug("Checking mock should save", metadata: [
"traceUrl": .string(request.url?.absoluteString ?? "")
])
var request = request
let request = request

let shouldSave = executeMockFilterForShouldSave(for: request, scenario: flags.scenario ?? "", statusCode: status, mockFilters: decider.mockFilters)

Expand Down Expand Up @@ -527,17 +531,53 @@ extension MockingStarCore: ServerMockSearchHandlerInterface {
}
}

// MARK: - Partial Mock Application
extension MockingStarCore {
func applyPartialMocks(to bodyData: Data, request: URLRequest, flags: MockServerFlags) async -> Data {
let partialDecider = await partialMocksActor.decider(for: flags.domain)
let matching = await partialDecider.findMatchingPartialMocks(request: request, deviceId: flags.deviceId)
guard !matching.isEmpty else { return bodyData }

logger.info("Applying \(matching.count) partial mock(s) to response for \(request.httpMethod ?? "?") \(request.url?.path() ?? "?")", metadata: [
"traceUrl": .string(request.url?.absoluteString ?? "")
])

let context = RequestContext(
domain: flags.domain,
method: request.httpMethod ?? "GET",
requestPath: request.url?.path() ?? ""
)

var result = bodyData
for pm in matching {
result = await partialDecider.applyModifications(to: result, modifications: pm.modifications, context: context)
}
return result
}
}

// MARK: - ScenarioHandlerInterface
extension MockingStarCore: ScenarioHandlerInterface {
public func addScenario(scenario: ScenarioModel) async throws {
await scenariosActor.decider(for: scenario.mockDomain).addNewScenario(scenario)
}

public func removeScenario(scenario: ScenarioModel) async throws {
await scenariosActor.decider(for: scenario.mockDomain).removeScenarios(deviceId: scenario.deviceId)
}
}

// MARK: - PartialMockHandlerInterface
extension MockingStarCore: PartialMockHandlerInterface {
public func addPartialMock(partialMock: PartialMockModel) async throws {
await partialMocksActor.decider(for: partialMock.mockDomain).addPartialMock(partialMock)
}

public func removePartialMock(partialMock: PartialMockModel) async throws {
await partialMocksActor.decider(for: partialMock.mockDomain).removePartialMocks(deviceId: partialMock.deviceId)
}
}

// MARK: - Import
extension MockingStarCore: MockingStarCoreInterface {
public func importMock(url: URL, method: String, headers: [String: String], body: Data?, flags: MockServerFlags) async throws -> MockImportResult {
Expand Down
Loading
Loading