Skip to content

crm_sdk/testing.dart

The fake server and client for your tests and demo builds.

import 'package:crm_sdk/testing.dart';
NameWhat it is
FakeCrmClientPurrCRM, in memory: its guest routes, at the root (/guest/v1/..., as on PurrCRM’s own host) or under /crm (a CRM whose routes are under a path prefix), and the storage the messenger’s pictures go to, all answered by server, on whatever host the request names.
FakeCrmHostThe platform around the fake PurrCRM.
FakeCrmNoticeSomething PurrCRM tells a guest that they have not seen: a reply in a conversation nobody was waiting on, which the platform keeps as a notice and pushes to the guest’s phones.
FakeCrmRequestOne request to the fake, as a client hands it on.
FakeCrmResponseWhat the fake answers: a status, and a JSON body or none.
FakeCrmServerThe fake PurrCRM.
FakeCrmTestOne test staff sent to a test phone, and what the guest did with it.
StandaloneCrmHostA host of the fake’s own, for a FakeCrmServer with no product’s fake around it: a package’s tests, or a demo build of a product.
crmFixtureDecodersEvery fixture a model of the package’s reads, by file name, with the model’s decoder.
fakeMessengerLayoutA home the app’s staff published, for a demo and a test of the fake (FakeCrmServer.messengerLayout): a greeting with the guest’s name, Alex and Sam’s faces with their names, an away message, a status, the send card, a link into the product, the help center’s search and its featured articles, then the guest’s conversations and tickets.
fakeNewsTopicThe fake’s News topic’s id.
fakeOffersTopicThe fake’s Offers topic’s id.
class FakeCrmClient extends BaseClient

PurrCRM, in memory: its guest routes, at the root (/guest/v1/..., as on PurrCRM’s own host) or under /crm (a CRM whose routes are under a path prefix), and the storage the messenger’s pictures go to, all answered by server, on whatever host the request names. Hand it to the facade as CrmConfig.httpClient, with any apiBase whose path is empty (base, say) or /crm:

final server = FakeCrmServer();
final PurrCRM = Crm(
config: CrmConfig(
apiBase: FakeCrmClient.base,
appVersion: '1.0.0',
productName: 'Acme',
httpClient: FakeCrmClient(server),
),
auth: CrmAuth.tokens(myTokens),
);

The guest is whoever the bearer token names to the server’s host: with a StandaloneCrmHost, the token itself.

FakeCrmClient(FakeCrmServer server, {Duration latency = Duration.zero})
static final Uri base

Where a facade over the fake can say the platform is. Nothing is ever sent there: the client answers every request itself.

Duration latency

How long every answer takes. A demo build sets a little, so that the screens’ waiting states can be seen.

bool offline

Every request fails as a network failure would, as on a phone with no connection.

final List<FakeCrmRequest> requests

Every request PurrCRM was sent, oldest first: a wait once it has been let go.

final FakeCrmServer server
void close()

Answers nothing more. The server goes on: close it too (FakeCrmServer.close) to let go of the waits it holds.

abstract interface class FakeCrmHost

The platform around the fake PurrCRM.

DateTime get now

What time it is, which every answer is written in.

String? guestFor(String token)

The guest the bearer token is for; null for a token nobody holds, or one that has run out, which PurrCRM answers with a 401 token_expired.

bool isOn(CrmFeature feature)

Whether the app has feature on: PurrCRM answers a route of a part that is off with a 404.

DateTime joinedAt(String guestId)

When guestId signed up: every guest of the fake is a test phone, added then.

bool productEventsOff(String guestId)

Whether guestId turned the product events off (“Help improve …”): PurrCRM keeps none of theirs.

void tell(String guestId, FakeCrmNotice notice)

PurrCRM telling guestId of notice, as PurrCRM’s notices and pushes would.

final class FakeCrmNotice

Something PurrCRM tells a guest that they have not seen: a reply in a conversation nobody was waiting on, which the platform keeps as a notice and pushes to the guest’s phones.

const FakeCrmNotice({required CrmPushType type, required String title, String body = '', String link = ''})
final String body
final String link

PurrCRM’s page it opens (/messages/<id>), as PurrCRM writes its links.

final String title

The line a notification shows (“Alex replied”), and the words under it.

final CrmPushType type

What kind of push it is: a support_reply, for a reply.

final class FakeCrmRequest

One request to the fake, as a client hands it on.

const FakeCrmRequest({required String method, required List<String> path, Map<String, String> query = const {}, Map<String, String> headers = const {}, String body = ''})
final String body

The body as text; empty when there was none.

final Map<String, String> headers

The headers, with lower-case names.

final String method
final List<String> path

The path within PurrCRM, a segment each: ['guest', 'v1', 'checkin'] for what PurrCRM serves as /guest/v1/checkin, whatever prefix the client found it under.

final Map<String, String> query
bool get isWait

Whether this is a conversation’s wait, which PurrCRM holds until something arrives (FakeCrmServer.hold).

final class FakeCrmResponse

What the fake answers: a status, and a JSON body or none.

const FakeCrmResponse(int status, [Object? json, Map<String, String> headers = const {}, bool problem = false])
final Map<String, String> headers

Headers beyond the content type.

final Object? json

The body, to be written as JSON; null for none.

final bool problem

Whether the body is a problem (application/problem+json).

final int status
Map<String, String> get allHeaders

Every header to answer with: the content type, when there is a body, and headers.

List<int> get bytes

The body as it goes on the wire.

class FakeCrmServer

The fake PurrCRM. See the library’s comment.

FakeCrmServer({FakeCrmHost? host, Uri? storageUrl, String productName = 'Acme', String appKey = defaultAppKey, Map<String, Object?>? orderRef = defaultOrderRef, bool unverified = false, Set<String> customEvents = const {}})

A server over host (a StandaloneCrmHost when not given) whose pictures go to storageUrl, which names the product productName, is the app appKey and knows the product’s own events customEvents beside the package’s.

final String appKey

The key of the app the fake is (CrmConfig.appKey), defaultAppKey unless told. Any other key is refused, and so is none, so that a product that forgot its key finds out. A test key (app_test_…) makes the fake a test app: its public face says mode test, which Crm.isTestMode reads.

final Set<String> customEvents

The product’s own events, by their words, which PurrCRM takes beside the package’s (CrmEvent).

static const String defaultAppKey

The key the fake is unless told.

static const Map<String, Object?> defaultOrderRef

The order reference the fake’s app has unless told.

static final Uri defaultStorageUrl

Where pictures go unless the server is told otherwise.

final List<String> emailClicks

The email clicks the app counted itself (POST /public/v1/email/c/{token}), by their tokens, oldest first.

final List<EventBody> events

Every product event PurrCRM kept, oldest first: each batch’s events from a guest who had not turned them off, once each by id.

bool helpBot

Whether a new conversation is met by the help bot; off, Alex answers every message.

String linkBase

Where the app’s links are, as its public face says: empty for an app with no link domain, the fake’s own unless told.

static const String linkMessageId

The post only its link opens (/messages/show/<id>): never in a check-in, shown by Crm.showMessage.

final List<AppLinksReport> linkReports

The links the app’s builds declared (PUT /guest/v1/app-links), oldest first.

Map<String, Object?>? messengerLayout

The messenger’s home as the app’s staff published it, sent to every guest as it is, but for its send card while the guest may not start a conversation; null for none, which is a home as it always was (fakeMessengerLayout is one). Its options are held as PurrCRM holds them: a start under never, or under when_none_open while one is open, is 409 refused (and the home has no opening), and a picture with attachments off is 422 attachment_invalid.

Duration messengerWaitFor

How long a conversation’s wait is held with nothing new before it answers with nothing, where PurrCRM holds it for messengerWait: short, so that a demo build’s conversation comes round often.

final Map<String, Object?>? orderRef

The app’s order reference, which the help bot’s question about an order carries, as JSON; null for an app without one.

final String productName

What the help bot, the survey and the welcome post call the product.

Duration? staffReplyAfter

How long after the guest writes Alex answers; null for never, which a test that answers by itself (staffReply) wants.

final Uri storageUrl

Where PurrCRM’s object store takes the messenger’s pictures: a picture’s upload names it, and a client posts the form there (store).

final bool unverified

Whether the fake’s app takes unverified guests: a request with no Authorization that names its guest in X-CRM-User-Id is that guest’s, as the host’s guestFor reads the id. Off, such a request is refused, as PurrCRM refuses it.

Map<(String, String), bool> get articleRatings

What each guest said about each help article, by guest id and slug.

bool get closed

Whether close has been called.

FakeCrmHost get host

The platform around it.

DateTime get now

The host’s time, in UTC.

FakeCrmResponse? answer(FakeCrmRequest request)

What PurrCRM answers request with, or null for a path it does not serve.

void close()

Answers every wait and stops every timer: nothing of the server’s is left running.

void closeConversation(String id)

Staff closing the conversation id, which asks the guest to rate it.

Map<String, Object?> contentOf(MessageKind kind, {String? look})

The fake’s own content of its message of kind, as PurrCRM sends it: to send a test of it changed (sendTest’s content). The fake has two banners and two surveys: look picks one by its banner style (floating, inline) or survey format (small, large); without it the first, the floating banner and the small survey.

List<String> conversationsOf(String guestId)

The ids of guestId’s conversations, the most recently changed first: on a guest’s first look, the three every guest finds (one open with an unread reply from Alex, one closed asking for a rating, and one about the ticket in progress).

Map<String, CrmDeviceBody> devicesOf(String guestId)

The devices guestId registered for PurrCRM’s pushes, by token.

Map<String, Object?>? exportOf(String guestId)

PurrCRM’s part of guestId’s export, as a host’s export carries it: their conversations and tickets, and with in-app messages on, their test phone, tests, receipts and survey answers. Null while neither the messenger nor in-app messages is on, when PurrCRM is not asked.

void forget(String guestId)

Everything of guestId’s, for an account deleted.

Future<FakeCrmResponse?> handle(FakeCrmRequest request)

request, answered: held first while it is a wait.

Future<void> hold(FakeCrmRequest request)

Holds a conversation’s wait until a part arrives or messengerWaitFor runs out, as PurrCRM holds it; at once when there is one already, when the server is closed, or when the wait is not the guest’s own (the route then refuses it). Anything but a wait is not held.

static FakeCrmResponse notServed(String path)

What PurrCRM answers a path it does not serve with.

String notice(String guestId, {CrmPushType type = CrmPushType.crmMessage, required String title, required String body, String link = ''})

A notice in guestId’s inbox, as PurrCRM keeps one with each push it sends for an app that delivers its own: its id. The fake pushes nothing; a test hands the push to the facade itself.

bool offersByPushOf(String guestId)

guestId’s consent to offers by push, as PurrCRM keeps it.

CrmPreferences preferencesOf(String guestId)

guestId’s preferences, as PurrCRM keeps them: the fake’s two topics, Offers (fakeOffersTopic: email, push and in the app, on by default but for email) and News (fakeNewsTopic: push and chat, off by default).

void resetMessages(String guestId)

Forgets what guestId did with the fake’s in-app messages (seen, dismissed, answered) and when they last checked in, so that the next check-in hands every one again, as on their first: for a test bench such as the example’s Message lab. Their conversations, notices and the tests staff sent stay. PurrCRM itself never forgets this way.

FakeCrmTest sendTest(MessageKind kind, {Map<String, Object?>? content})

Staff sending a test of the fake’s message of kind (its banner, survey, post or carousel) to a test phone: the next guest who checks in is handed it first. content in place of the fake’s own (a carousel of another presentation, say), as PurrCRM sends the kind’s content. The test, to see what became of it.

void staffReply(String id, String text, {String name = 'Alex'})

A reply from a member of staff (name) in the conversation id, as PurrCRM’s inbox would send one: it wakes the conversation’s waits, and is a support_reply notice to the guest (FakeCrmHost.tell) when nobody is waiting on it. As PurrCRM’s does, it opens a closed conversation again, with a line saying so by the one replying, and wakes a snoozed one.

FakeCrmResponse store(String body)

A form posted to the storage (storageUrl): the picture its key names is uploaded.

final class FakeCrmTest

One test staff sent to a test phone, and what the guest did with it.

final Map<String, SurveyAnswer> answers

A survey test’s answers, by question.

bool clicked
bool completed
bool dismissed
String? guestId

The guest it was handed to; null until a check-in took it.

final String id
final MessageKind kind
bool seen
final DateTime sentAt

When staff sent it.

class StandaloneCrmHost implements FakeCrmHost

A host of the fake’s own, for a FakeCrmServer with no product’s fake around it: a package’s tests, or a demo build of a product.

  • Any bearer token is a guest, and the same token the same guest: a host hands the facade the token it wants to be.
  • Every one of PurrCRM’s parts is on unless features says otherwise; a test may change it at any time.
  • A guest’s notices are kept in notices, and each is pushed on pushes as FCM would hand it to the phone, for a demo to pass to Crm.handlePushArrival.
StandaloneCrmHost({DateTime Function()? clock, Iterable<CrmFeature> features = const [CrmFeature.messenger, CrmFeature.helpCenter, CrmFeature.inAppMessages]})
final Set<String> eventsOff

The guests who turned the product events off.

final Set<CrmFeature> features

PurrCRM’s parts that are on.

final List<({String guestId, String id, FakeCrmNotice notice})> notices

Every notice PurrCRM sent, oldest first, with the guest it was for.

Stream<Map<String, String>> get pushes

The data of every push, as FCM would hand it to the phone: its type, its notice’s id and its link.

Future<void> close()

Ends pushes.

String? guestFor(String token)

The token itself; a guest joins the first time their token is seen.

final Map<String, Object Function(Map<String, Object?>)> crmFixtureDecoders

Every fixture a model of the package’s reads, by file name, with the model’s decoder.

const Map<String, Object?> fakeMessengerLayout

A home the app’s staff published, for a demo and a test of the fake (FakeCrmServer.messengerLayout): a greeting with the guest’s name, Alex and Sam’s faces with their names, an away message, a status, the send card, a link into the product, the help center’s search and its featured articles, then the guest’s conversations and tickets.

const String fakeNewsTopic

The fake’s News topic’s id.

const String fakeOffersTopic

The fake’s Offers topic’s id.