Skip to content

About

Mock Files.com Server API for your own Integration Testing

Resources

Security policy

Stars

20 stars

Watchers

6 watching

Forks

Repository files navigation

Files.com Mock API Server (in Ruby)

This serves a mock Files.com API server, which is useful for testing things like the Files.com SDKs and other direct integrations against the Files.com API.

Files.com is the cloud-native, next-gen MFT, SFTP, and secure file-sharing platform that replaces brittle legacy servers with one always-on, secure fabric. Automate mission-critical file flows—across any cloud, protocol, or partner—while supporting human collaboration and eliminating manual work.

With universal SFTP, AS2, HTTPS, and 50+ native connectors backed by military-grade encryption, Files.com unifies governance, visibility, and compliance in a single pane of glass.

The server has two modes, chosen when it starts:

  • Legacy mode (the default) is a simple Grape app with generated definitions for each API endpoint. It checks required parameters and parameter types, then returns a fixed example response. It does not maintain state and it does not deeply inspect your submissions for correctness. This is useful for testing basic network operations and JSON encoding for your SDK or API client.
  • Simulation mode (opt-in) keeps records, files and folders in memory, so a test can create, find, update, list and delete any regular API resource, page through results, upload a file and download the same bytes, copy, move and delete folders, and make chosen requests fail, stall or lose their connection on purpose, reproducibly. Requests it does not simulate fail with a clear error instead of returning an example response.

Neither mode checks credentials. Send any placeholder API key; never use real Files.com credentials with the mock server.

Requirements

To run this server with Ruby (Local Ruby Usage):

  • Ruby 3.3.12 or newer. bundle install installs the gem versions in Gemfile.lock, and some of them require Ruby 3.3 or newer.
  • Bundler

To run it in Docker (Docker Image Usage), you need only Docker: the image brings its own Ruby (this directory's Dockerfile uses Ruby 3.4.11).

Generating This Server

This directory is generated by the Files.com SDK generator from one Swagger document. To build the same server again, run the generator from a clean checkout with that document, pinned by its SHA-256:

shasum -a 256 public.json          # compare with the SHA-256 you pinned
ruby files-sdk-generator.rb --target mock-server --file public.json

The server records what it was generated from in lib/simulation/generation.json: the Swagger document's SHA-256, the generator's commit and whether its sources were clean, and the SHA-256 of the schema subset it validates requests against. Its readiness report (GET /__files_mock/v1/ready) repeats that schema's SHA-256, so a test can check that the running server is the one it expects. A server generated from another document, or from a changed generator, is another server.

Local Ruby Usage

Install dependencies once:

bundle install

Start the legacy server, which listens on port 4041 on all IPv4 interfaces (0.0.0.0):

bundle exec puma

Start the simulation server, which listens on 127.0.0.1:4041:

FILES_MOCK_MODE=simulation bundle exec puma

FILES_MOCK_MODE is read once at startup. Leaving it unset, empty, or set to legacy starts the legacy server. Any other value stops startup with an error, so a typo cannot silently start the wrong server.

To choose another port while keeping the simulation server on loopback, pass Puma's -b option, for example -b tcp://127.0.0.1:4051. Read Ports and interfaces before using -p or your own Puma configuration file.

Docker Image Usage

The published filescom/files-mock-server:latest image is a moving tag: it is built from whatever server was last published, so it need not contain the simulation mode, schema or contract version this directory describes. To run exactly this server in Docker, build the image from this generated directory and run it by the image ID you recorded, not by a tag:

docker build -t "files-mock-server:$(cat _VERSION)-local" .
image=$(docker image inspect --format '{{.Id}}' "files-mock-server:$(cat _VERSION)-local")
echo "$image"   # record this ID (sha256:...) with your test results
docker run -p 40410:4041 -it "$image"

The server in the image is this directory's, so its generation receipt (lib/simulation/generation.json) and its readiness report (below) say which schema it was generated from; check them before you rely on a run.

The image starts from the official ruby:3.4.11-bookworm image (Ruby 3.4.11 on Debian 12), pinned by its digest, and keeps that image's RubyGems. It installs the Bundler that Gemfile.lock names (4.0.21), then exactly the locked gems, frozen: a Gemfile that disagrees with Gemfile.lock fails the build instead of changing the lock. The pin stops the base from changing between builds; it does not keep the base's operating system, Ruby or RubyGems up to date. Only linux/amd64 builds have been checked.

Before the gems, the build applies the security fixes the pinned base does not have yet. image-updates.sh pins each one by version and SHA-256: Debian 12's OpenSSL 3.0.22 (libssl3, libssl-dev, openssl) and libde265 1.0.11-1+deb12u3 from Debian's security archive, and json 2.19.9 from rubygems.org. json 2.19.9 replaces Ruby 3.4.11's default json 2.9.1 (CVE-2026-54696), whose files the build removes, so Ruby run outside the bundle loads the fixed version; the server itself runs with the json Gemfile.lock names. When Debian replaces these package versions with newer fixes, the pins in image-updates.sh need updating: an online build fails once Debian's archive no longer serves the pinned versions.

To build without network access, fill vendor/cache first. In this directory, on a machine with network access, curl and Ruby 3.3.12 or newer:

gem install bundler --version 4.0.21
BUNDLE_FROZEN=true bundle cache --no-install
(cd vendor/cache && gem fetch bundler --version 4.0.21)
sh image-updates.sh fetch vendor/cache

bundle cache collects every gem Gemfile.lock names, but not Bundler itself, which gem fetch adds; image-updates.sh fetch downloads the pinned Debian packages and json gem and checks each SHA-256. When vendor/cache exists, the build installs the security fixes only from it, and a missing file or one with another SHA-256 fails the build. It then installs Bundler from its cached archive and uses cached gems or gems already installed in the base. A dependency missing from both fails instead of being fetched from rubygems.org. Provide the whole cache with the commands above, or remove the directory to use online installation. The cached Debian packages are amd64 builds, so a build from vendor/cache is for linux/amd64 only (docker build --platform linux/amd64 on other machines); an online build installs the same versions for its own architecture. Only the image's installation steps are offline: the Docker daemon still needs the base image, and may contact the registry to resolve it even when the image is already present.

Example:

curl 127.0.0.1:40410/api/rest/v1/users

To run the simulation server in Docker, tell it to listen on the container's interfaces and publish the port on your machine's loopback address only:

docker run --rm -e FILES_MOCK_MODE=simulation -e FILES_MOCK_TRANSFER_ORIGIN=http://127.0.0.1:40410 -p 127.0.0.1:40410:4041 "$image" -b tcp://0.0.0.0:4041

FILES_MOCK_TRANSFER_ORIGIN tells the simulator the address your tests use, so the upload and download URLs it returns point at the published port (see Upload and download URLs).

In CI, a simulation server can run as a service container bound to tcp://0.0.0.0:4041 when the job network is isolated to that job. Wait for GET /__files_mock/v1/ready, check that it reports the schema you expect, then reset it with your fixtures and profile before each scenario:

docker run --rm -d --name files-sim -e FILES_MOCK_MODE=simulation -e FILES_MOCK_TRANSFER_ORIGIN=http://127.0.0.1:40410 \
  -e FILES_MOCK_MAX_BODY_BYTES=8388608 -p 127.0.0.1:40410:4041 "$image" -b tcp://0.0.0.0:4041
until curl -fs 127.0.0.1:40410/__files_mock/v1/ready > /dev/null; do sleep 0.2; done
curl -s 127.0.0.1:40410/__files_mock/v1/ready | ruby -rjson -e 'puts JSON.parse(STDIN.read).values_at("contract_version", "schema_sha256")'
curl -s -X POST 127.0.0.1:40410/__files_mock/v1/reset -H 'Content-Type: application/json' -d '{"fixtures": {"users": [{"username": "alice"}]}}'

For https URLs, mount a certificate for 127.0.0.1 and bind TLS inside the container, for example -v "$PWD/tls:/tls:ro" ... -b 'ssl://0.0.0.0:4443?key=/tls/key.pem&cert=/tls/cert.pem&verify_mode=none' with FILES_MOCK_TRANSFER_ORIGIN=https://127.0.0.1:40443 and -p 127.0.0.1:40443:4443.

Ports and interfaces

The bundled config/puma.rb chooses the interface for each mode: the legacy server listens on all IPv4 interfaces (tcp://0.0.0.0:4041) and the simulation server on loopback (tcp://127.0.0.1:4041). Puma's -b option replaces that bind, so -b tcp://127.0.0.1:4051 moves the simulation server to another port while keeping it on loopback.

Puma's -p PORT option also replaces the configured interface, including the simulation server's loopback default. It binds :: (all IPv6 interfaces, which on most systems also accept IPv4 connections) when the machine has a non-loopback IPv6 interface, and 0.0.0.0 otherwise. Use -b tcp://0.0.0.0:PORT for an IPv4-only bind.

The simulation server's loopback default, its refusal to run with Puma workers, Puma's own request body limit and the way Puma reads request bodies (see Limits) all come from the bundled config/puma.rb. If you start Puma with your own configuration file instead, bind to loopback, run without workers, and set http_content_length_limit and queue_requests false yourself.

Simulation Mode

Everything below applies only when FILES_MOCK_MODE=simulation.

Supported behavior (contract version 3)

Area Simulated Refused (501) or not simulated
Records List, create, find, update and delete of every resource with the regular /{name} and /{name}/{id} shape, records without an id kept under the parent they were created for, group memberships found by their own fields, lists scoped by path or owner, Style records keyed by path, log lists and delete-only resources filled by fixtures, configuration-dependent answers and a key lookup from fixtures, and the site singleton (GET/PATCH /site). Increasing IDs per resource, exact types, write-only secrets never returned, file parameters decoded, checked and reported. Actions (such as /users/{id}/unlock), each with the decision or real owner it needs, filtering, sorting, search, uniqueness, foreign keys. GET /__files_mock/v1/inventory lists every operation and why.
Files and folders Uploads (known size, parts in order or not, per the upload profile), finalize with or without etags, whole and ranged downloads, stat, metadata, PATCH /files/{path} metadata, folders, listing, delete, recursive delete, copy and move with FileAction results, pending actions with FileMigration polling, ZIP listing, extraction and saving (file_actions/zip), file and folder fixtures, underscore destinations with their scope journaled. Remote server mounts, lock enforcement (locks are advisory), permissions, file previews (with_previews lists a page of folders only), copy_behaviors, other spellings of existing paths, the stores underscore destinations name.
Locks Advisory locks on paths, found as the model finds them (by byte prefix): created when both public flags are true, listed with the locks above that apply to subfolders and, on request, the path's range one level deep, as one sorted array, released with their token, exclusive conflicts with the model's message, expiry on a lock clock the tests move, and a separate cleanup of expired locks. Refreshing a lock or anything else its internal parameters do, shared or owner-only locks, who may act on another user's lock, the real removal job's timing.
Transfer profiles Legacy (default), serial and parallel part rules, retries forbidden, advertised part sizes (hints) or variable part limits, scheduled part size changes for parts not yet issued, part offsets, target class, a shared part concurrency limit with 503 throttling, presigned URLs, the download identity contract, the historical download request status protocol (an ID on each successful storage response and a chosen status at the download URL joined with it), withheld download sizes, cursors with special characters. Provider-specific limits beyond the profile, adaptive tuning or throughput claims.
Faults Deterministic and seeded random rules per operation, record, path, part, session and attempt: Files.com errors with chosen types and data, unstructured and web-tier answers, redirects, expired URLs, frontend HTML, empty pages, delays before or after a request is applied, dropped connections before or after a commit, truncated, short, padded and stalled bodies. Faults outside the local HTTP server's reach, such as TLS handshake failures.
Observation A bounded journal of every API and transfer request: operation, status, wire types, fault used, identity headers by number (never values), user agent, timing, bytes and digests, file parameters by filename and digest, the store an underscore destination names, and each user listing's paging values and selected cursor headers by type and digest. Real authentication, permissions and session semantics.

Anything a test needs a real Files.com site for (authentication, permissions, workspaces' effects, providers, performance) stays a real or native check; a pass here never stands in for it.

Simulated operations

Operation Request Success
users.create POST /api/rest/v1/users 201 and the new user
users.list GET /api/rest/v1/users 200 and an array of users
users.find GET /api/rest/v1/users/{id} 200 and the user
users.update PATCH /api/rest/v1/users/{id} 200 and the updated user
users.delete DELETE /api/rest/v1/users/{id} 204 with an empty body
files.begin_upload POST /api/rest/v1/file_actions/begin_upload/{path} 200 and an array of upload parts, one unless parts asks for more
files.finalize_upload POST /api/rest/v1/files/{path} with action: "end" 201 and the new file, 200 when it replaces one
files.download GET /api/rest/v1/files/{path} 200 and the file with a download_uri; with action=stat, the file or folder without one
files.metadata GET /api/rest/v1/file_actions/metadata/{path} 200 and the file or folder
files.delete DELETE /api/rest/v1/files/{path} 204 with an empty body
folders.create POST /api/rest/v1/folders/{path} 201 and the new folder
folders.list GET /api/rest/v1/folders/{path} 200 and an array of the folder's files and folders
files.copy POST /api/rest/v1/file_actions/copy/{path} 201 and a FileAction
files.move POST /api/rest/v1/file_actions/move/{path} 201 and a FileAction
file_migrations.find GET /api/rest/v1/file_migrations/{id} 200 and the FileMigration of a pending copy, move, unzip or ZIP
files.update PATCH /api/rest/v1/files/{path} 200 and the file or folder with the metadata it sets
files.zip_list GET /api/rest/v1/file_actions/zip_list/{path} 200 and the ZIP file's entries (path, size); only when the Swagger document declares it
files.unzip POST /api/rest/v1/file_actions/unzip 201 and a pending FileAction; its FileMigration extracts the entries; only when the Swagger document declares it
files.zip POST /api/rest/v1/file_actions/zip 201 and a pending FileAction; its FileMigration saves a ZIP of paths as destination; only when the Swagger document declares it
locks.list_for GET /api/rest/v1/locks/{path} 200 and an array of the locks that apply to the path; see Locks
locks.create POST /api/rest/v1/locks/{path} 201 and the new lock
locks.delete DELETE /api/rest/v1/locks/{path}?token=… 204 with an empty body

The three lock operations are simulated when the Swagger document declares all of them and the Lock object. Every other regular resource is simulated as records; see Record resources. Uploaded bytes go to the upload_uri each upload part names, and downloads come from the download_uri; see Files and Folders.

Send parameters the way the Files.com SDKs do: in the query string for GET and DELETE, and as a JSON object body with Content-Type: application/json for POST and PATCH. A form body (application/x-www-form-urlencoded), as the Ruby SDK sends, is read within FILES_MOCK_MAX_BODY_BYTES and parsed by Rack within its nesting depth and parameter count limits: names such as etags[][part] make arrays and objects, and every value is a string, as in a query string. A body value replaces a query value of the same name. A body that does not parse gets 422 bad-request/invalid-body, and a query string that does not parse gets 422 bad-request.

Any other request under /api/rest/v1/ returns 501 with the type simulation/not-supported (a HEAD request gets the same status with an empty body), and so does a simulated operation that sends a parameter the Files.com API schema declares but the simulator does not model. The readiness response below lists the simulated operations.

How users behave:

  • IDs start at 1 and increase by one for each created user. A deleted user's ID is never reused until the next reset.
  • A created user contains its id and the fields you supplied that are part of the User object. Fields the real API computes or defaults (such as created_at) are not simulated.
  • An update changes only the fields you supply and keeps the ID and every other field. Sending null for an optional field clears it.
  • Finding, updating, or deleting a user that does not exist returns 404 with the type not-found, like the Files.com API.
  • password, password_confirmation, change_password, change_password_confirmation and imported_password_hash are checked to be strings and then discarded. They never appear in responses or in the journal.
  • Parameters are checked against the Files.com API schema before anything changes: required parameters, strings, whole numbers (including 32-bit ranges), true/false, enumerated values such as ssl_required and authentication_method, and date-times such as authenticate_until. Date-times must be ISO 8601 with a UTC offset (for example 2030-01-02T03:04:05+02:00) and are returned in UTC (2030-01-02T01:04:05Z), to whole seconds: fractional seconds are discarded, so 2030-01-02T03:04:05.750Z is returned as 2030-01-02T03:04:05Z. Dates and times that do not exist, such as February 30 or 24:00, are rejected rather than rolled over. Invalid values return 422 with the type bad-request, and nothing is created or changed.
  • avatar_file is a file parameter (see File parameters). Schema-declared parameters with effects the simulator does not model, such as group_id or new_owner_id, return 501.
  • Keys the schema does not declare for the operation are ignored, as the Files.com API ignores them. A misspelled parameter name, such as perpage, is therefore not reported.
  • The simulator does not check credentials, username uniqueness, or whether IDs in one record refer to other records.

Record resources

Every resource whose operations have the regular shape, a collection at /api/rest/v1/{name} (list, create) and members at /api/rest/v1/{name}/{id} (find, update, delete) answering with one entity that has an id, is kept as records the way users are:

  • IDs start at 1 for each resource, increase by one and are never reused before a reset.
  • An id the schema types as a string (chat sessions, DNS records, IP address lists) is the record's own data instead: a fixture supplies it, lists and finds return it exactly as given (left out when unset, null when null), and GET /api/rest/v1/{name}/{id} finds the record holding it, the segment decoded once. No two records may hold the same one. The simulator never makes one up, and such ids name no real chat, DNS record or address. Lists still page in the order records were loaded.
  • A record holds exactly what was supplied: a field never set is left out of responses, an explicit null is kept as null, and false, 0 and empty strings stay as sent. Int64 values keep every digit, decimals stay exact strings (a JSON number is kept at its exact value), and arrays and objects keep their elements. A decimal string may use an ordinary e or E exponent (1E-7, 1.5E+3), which the API's parameter coercion reads, and is kept as sent, never expanded or converted through a float; other spellings (.5, 12., +1, 1_000, surrounding spaces) get 422. How the API spells a decimal it returns, and what a field's precision and scale keep, are not simulated. Each element of a decimal array is checked the same way. Nothing is derived from other fields. A JSON number's exact plain form may add at most 32 zeros to its significant digits (1e32 is kept whole): past that, a decimal parameter gets 422 before anything changes, and a number inside an untyped object keeps its exact value in exponent form (0.1e100001), so no number's text grows much beyond the bytes that carried it.
  • Parameters are checked against the schema before anything changes: types, 32-bit ranges, enumerated values, dates and date-times, and arrays of them. A parameter that is not one of the record's fields is accepted only when its name marks a secret (such as password, token or private_key): it is type-checked, then never stored, returned or journaled. Any other one gets 501.
  • An empty string sent for an array parameter is an empty array, as the API's parameter coercion reads an array of strings, numbers or objects ([Hash]), such as an automation's import_urls. Form field sets' form_fields is the exception: the API declares it as nested JSON objects, whose blank string (any blank line) is no value, so it is kept as an explicit null is, and whose other strings are parsed as JSON: an encoded array of objects, or one object (wrapped in an array), is kept as the same array sent directly, numbers included, and "[]" and "null" are what they encode. An encoded value that is malformed, a scalar or holds a member that is not an object gets 422 bad-request (form_fields is invalid), unlike a malformed request body (bad-request/invalid-body); an object sent directly is not wrapped. Any other string, and a member of the wrong type, gets 422. Only requests are read this way, with the fixture records a resource's create makes; fixture responses and the other fixtures are checked exactly as given. What Files.com's form field model then does with the fields (their ids, defaults and rules) is not simulated.
  • A required parameter sent null is there, so the API's parameter validation admits it; only one left out is 422 bad-request (name is missing). For a share group's create, the model's own validations then decide, as Files.com's ShareGroup does: a null or blank name, and null or empty members, fail the save with 422 processing-failure/model-save-error and the model-error body (error, model_errors by field, model_error_keys such as members_cannot_be_empty, errors), and nothing is kept. A name sent null never hides another parameter's framework error. The member models and the user, site and workspace checks behind a save are not simulated, nor is a share group update's. Other resources still refuse a required parameter sent null as missing, since what their models do is not simulated.
  • List parameters other than cursor and per_page (filters, sorts, ids, search) get 501. Lists page by ID like users.
  • A resource whose entity has no id and that is only listed, such as the history and log lists, is filled by fixtures and paged in fixture order.
  • A list declared to answer one entity instead of an array (in the schemas here, only the decimal-compat fixture's GET /foos, which spec/fixtures/swagger_decimal_compat_new.json declares) answers the one record holding exactly the values its parameters send, a decimal's text and an array's elements in order included; none gets 404, and more than one 501. This is the simulator's reading of a synthetic schema's shape for the decimal-array catalogue lane, not the behavior of any Files.com endpoint.
  • The site is a singleton: GET /api/rest/v1/site returns what fixtures and PATCH /api/rest/v1/site set, checked with the update rules.
  • A required create parameter that names the parent a record belongs to (path, or a *_id that is not one of the record's fields, such as a bundle recipient's bundle_id or a comment's path) is kept as the record's structural scope. It is never returned and never checked against the parent: nothing verifies that bundle 7 or docs/a.txt exists. A list of such a resource requires its scope and returns only that scope's records, and its cursors are valid only for that same scope (and key-field selection); another one gets 422 bad-request/invalid-cursor. A reset fixture gives a record the scope its create would have carried with _scope, as in {"name": "legal", "_scope": {"path": "contracts"}}.
  • A required list parameter that names the parent scopes that list the same way. An automation run's automation_id is one of its fields, so the list answers the runs holding it. A history export result's history_export_id and an action notification export result's action_notification_export_id are not fields: fixtures set them with _scope, checked like the list parameter, and they are never returned. Left out, the parameter gets 422 bad-request.
  • Records without an id (bundle and inbox recipients) are kept in the order they were created. Group memberships have no id either: PATCH and DELETE /api/rest/v1/group_users/{id} find the membership by the group_id and user_id they require, and the {id} in the path selects nothing, because the GroupUser object has no id to compare it with (a recorded route-key decision). A delete-only resource (partner sites) holds what fixtures give it.

Lists scoped by path or owner

These lists return the records of one scope, by an explicit rule rather than general filtering: the behaviors of a folder (/behaviors/folders/{path}; with ancestor_behaviors, also those of the folders above it), comments on a file (/file_comments/files/{path}), requests of a folder (/requests/folders/{path}), history of a file, folder or user and login history (/history/...), and the metadata categories that apply to a path (/metadata_categories/list_by_path/{path}, assigned by fixtures with _scope, since folder behaviors the simulator does not evaluate decide it on Files.com). Their other parameters get 501.

Styles

/api/rest/v1/styles/{path} is keyed by path: GET finds the style, the first PATCH creates it (with an id from 1) and later ones change it, and DELETE removes it. The path is never checked against files and folders. A style's file is a file parameter.

Fixture responses

Read-only operations whose answer depends on configuration or providers the simulator does not hold (site usage, the automation authoring schema, an automation run's node, supported holiday regions, reserved IP addresses, a remote server's agent nodes and configuration file) answer only from a reset fixture under fixtures.responses, checked against the object's fields, id included, which is kept exactly as given: one answer, a list (paged like records), or answers keyed by the operation's required parameters joined with /, such as {"remote_servers.find_configuration_file": {"7": {"id": 7}}}. The route's values name the answer, whatever a query or body copy of the same name says. A required query parameter left out, such as a run node's node_id, gets 422 bad-request (node_id is missing) before the fixture is consulted, as the API's parameter validation answers it. Sent null or empty it passes that validation: an empty value is part of the key, so {"automation_runs.find_node": {"3/": …}} answers run 3 with an empty node_id, and a null names no key. Without a fixture they get 501, and a key with no answer gets 404. The simulator never invents their content.

GET /api/rest/v1/partner_site_requests/find_by_pairing_key, whose schema declares only 204 with no body, is a lookup: its fixture lists the synthetic keys that exist, as in {"partner_site_requests.find_by_pairing_key": ["key-a", "key-b"]}, and a request gets 204 for one of them and 404 for any other, a pairing_key sent null or empty included, since listed keys are non-empty strings; left out, it gets 422 bad-request first. That is existence only: nothing of real partner pairing, approval or authentication is simulated. It is the fixture's rule, not Files.com's: the service's lookup presents the partner site request it finds, and approval or rejection clears a request's key, so a null key can find one; what it answers for a key it does not find, and whether an answer is the 204 the schema declares, is not established here.

File parameters

A parameter the schema types as a file (avatar_file, attachment_file, a style's file) is accepted in the same request as its companion values, either as the JSON file object Files.com takes, {"encoded_content": BASE64, "filename": ..., "type": ...}, or as a part of a multipart/form-data request whose other parts carry the companions (nested ones as value[url]). Its bytes are decoded within the body limit, checked and discarded: the journal's attachments lists each one's parameter, filename exactly as sent (and the raw filename of a multipart header), content type, size, SHA-256 and encoding, never its bytes. Base64 that does not decode gets 422. Multipart cannot carry null or typed nested values (an empty part for an array parameter is an empty array, as above); send JSON when they matter.

Inventory

The readiness response names each record operation with its Swagger operation ID. GET /__files_mock/v1/inventory returns every operation of the Swagger document the server was generated from, with its disposition: record, singleton, scoped-list, path-record, fixture-response, specialized (users, files, folders, file migrations, locks), real-only (its result depends on who is authenticated, such as sessions and the current API key) or gap. A gap names its gap_kind and reason: action-semantics (the named owner must decide the operation's simulated contract), real-or-provider (outbound delivery or provider behavior, real lane only), excluded-auth (authentication state, excluded), incomplete-schema (a lock operation of a Swagger document that lacks another lock operation or the Lock object, which the simulator needs to simulate any of them) or shape. An operation with a recorded reason is refused even when its shape looks like a record's, such as POST /webhook_tests, which sends a request.

Pagination

GET /api/rest/v1/users returns users in ID order, which is the order they were created. per_page must be a whole number from 1 to 10000 and defaults to 1000. When more users remain, the response carries the same cursor in both the X-Files-Cursor and X-Files-Cursor-Next headers; pass it back as cursor, with the same per_page, to get the next page. The last page has neither header.

Cursors are opaque. A cursor is accepted only by the simulator process that issued it, with the same per_page, until the next reset; anything else returns 422 with the type bad-request/invalid-cursor.

A cursor continues after the last user it returned. Users created during a traversal appear on later pages, users deleted before they are reached are skipped, and no user is returned twice.

With the reset profile {"list_cursors": "special-characters"}, every cursor (records, scoped lists, folders and fixture responses) starts with 2:a+b &x=y%25#: a space, +, &, =, a percent escape and #. A client must send it back exactly, encoded once; one decoded, or encoded again, gets 422 bad-request/invalid-cursor. Cursors travel in headers, so they stay ASCII; a fixture response keyed by a query value (such as an automation run node's node_id) checks Unicode as well.

Filtering, sorting and search parameters (sort_by, filter, filter_gt, filter_prefix, ids, search, and the like) return 501 rather than an unfiltered list.

Files

Simulation mode keeps files and their bytes in memory, so a test can upload a file with a Files.com SDK and download exactly the same bytes, whole or as a single byte range.

A file path is a name in the simulator's own namespace: nothing is read from or written to the machine running the server. Paths keep their spaces, Unicode characters and percent signs. The path in a URL is decoded exactly once, so /api/rest/v1/files/folder/a%252Fb.txt names the file a%2Fb.txt in folder, and the Go SDK (which sends a path's slashes as they are) and the Python SDK (which encodes them as %2F) name the same file.

Paths are stored and returned exactly as sent. To find paths the Files.com API would treat differently, the simulator compares them as the API does, using the comparison map in shared/path_comparison.json (version 1, for MySQL's utf8mb4_0900_ai_ci), which comes from the Files.com server. It uses the comparison only to refuse paths it does not model; see Differences from the Files.com API.

A required parameter left out of a file action, such as a copy's destination, gets 422 bad-request with the message destination is missing, as the API's parameter validation (Grape) answers it. That validation checks only that the parameter is there: one sent as null or empty passes it and is judged by the action after the path rules below. A file action's destination, or an unzip's path, sent that way names the site root, as the API's path and destination helpers normalize it (see Copying and moving and ZIP files).

Every file, folder and lock route checks its path as the API's path helper does, after the request's declared parameters (so a value of the wrong type, or a copy, move or unzip without a destination, is answered first):

  • A path with a folder name ending in whitespace (a space, a tab or another ASCII whitespace character) gets 422 bad-request/path-cannot-have-trailing-whitespace.
  • A path holding a zero-width space (U+200B) anywhere then gets 422 bad-request/invalid-path. No other invisible character is refused.

On the folders endpoints (POST and GET /api/rest/v1/folders/{path}) every name in the path is a folder's, so the last one is checked too. On every other route the last name is the file's or lock's own and may end in whitespace. So docs/notes.txt (with a trailing space) can be uploaded, downloaded and locked, while docs /notes.txt and POST /api/rest/v1/folders/docs%20 are refused. An unzip's path is checked like a route's path; destinations follow the API's destination rule instead (see Copying and moving). A path in a form the API would rewrite still gets 501 first, and a folder name ending in whitespace is refused before a name the simulator refuses as ambiguous.

Uploading

The SDKs' upload methods take these steps for you:

  1. POST /api/rest/v1/file_actions/begin_upload/{path} starts an upload and returns 200 with an array of one upload part: its ref, part_number and the upload_uri to send that part's bytes to. Send the ref and the next part number to get the next part's URL. With parts: N the array holds N consecutive parts of the same upload, from part 1 for a new upload or from part with a ref (which may repeat parts already issued; they keep the size they were issued with). parts must be a whole number from 1, and the last part it asks for must be within the simulator's 64 parts, or it gets 422 or 409 before any folder, upload or part changes.
  2. PUT each part's raw bytes to its upload_uri, with any content type or none. The response is 200 with an ETag header: the SHA-256 of the part's bytes, in quotes. Sending a part again with the same bytes returns the same ETag; different bytes for a stored part get 501.
  3. POST /api/rest/v1/files/{path} with action: "end", the ref, and etags listing every part as {"etag": ..., "part": ...}. This publishes the file in one step and returns 201 and the file, or 200 when it replaces an existing file. A sequential upload of known size may leave etags out and send size instead, as the .NET SDK does; the parts the upload received then become the file.

size, when beginning or finalizing an upload, is a byte count: a whole number from 0 below 2^63, sent as a number or a string of digits. The production schema documents it as int32, but the API reads it as an Integer byte count, so the simulator does not hold it to 32 bits. Every other whole-number parameter, part numbers among them, keeps its schema's range.

Parts are joined in part number order, whatever order they arrived or were listed in, and a part number may be sent as a string ("2"), as the Go SDK does. Before anything is published, finalizing checks that the listed parts are numbered 1 to N with no gaps or repeats, that each was uploaded with the listed ETag, that no uploaded part is left out, and, when size is sent, that the parts add up to it. A failed check changes nothing: the upload stays open so it can be finalized again, and an existing file keeps its bytes. A finalized upload's ref is no longer valid, so a repeated finalize gets 404.

Problem when finalizing Status Type
No ref 422 bad-request/request-params-required
An unknown ref, or one for another path 404 not-found/file-upload-not-found
No parts, a listed part that was not uploaded, or a wrong ETag 422 processing-failure/file-not-uploaded
Part numbers with a gap or a repeat, or an uploaded part left out 422 bad-request/invalid-etags
A size the parts do not add up to 422 bad-request/request-params-invalid
A file where a folder is 422 processing-failure/destination-exists

Without etags, finalizing uses the parts the upload has received, in part number order, as Files.com's own storage finds an upload's parts. The simulator does this only for a sequential upload of known size: size must be sent and the received parts must be numbered 1 to N. A part still being received does not count. Without size, or with a gap in the received parts, finalizing gets 501, and with no part received it gets 422 processing-failure/file-not-uploaded. Sent etags are always checked as above and never replaced by the received parts: etags: [] gets 422 and etags: null gets 501.

An empty file is an upload of one empty part. provided_mtime accepts a time with a UTC offset, a time without one (as the Python SDK sends it, read as UTC), a UTC time in the fixed form 2026-09-24 00:00:00Z (as the .NET SDK sends it), or Ruby's 2026-09-24 07:11:22 -0700 and 2026-09-24 14:11:22 UTC (as the Ruby SDK sends it). Only the ISO form may leave out the offset. It is returned in UTC to whole seconds.

The upload parts advertise parallel_parts: false, retry_parts: true and a partsize of 5 MiB, or FILES_MOCK_MAX_BODY_BYTES when that is smaller. These tell a client how to upload; the simulator does not enforce them. It accepts parts sent at the same time or out of order and joins them by part number when the upload is finalized, so a passing test does not show that a client sends one part at a time. SDKs may send other part sizes, and the simulator accepts them: the Go SDK sends up to 5 MiB per part whatever partsize says, while the Python SDK follows partsize. An upload that exactly fills its last part may finish without an empty terminal part. The simulator also accepts an empty terminal part from clients that send one. Every part body must fit in FILES_MOCK_MAX_BODY_BYTES, so raise it to at least 5 MiB, for example FILES_MOCK_MAX_BODY_BYTES=8388608, before uploading files over 1 MiB with the Go SDK.

Downloading

GET /api/rest/v1/files/{path} returns the file with a download_uri, and GET /api/rest/v1/file_actions/metadata/{path} returns it without one. With action=stat, GET /api/rest/v1/files/{path} also returns the file without a download_uri, and it describes folders too: /api/rest/v1/files/%2F?action=stat is the root. Downloading a folder gets 422 with the type bad-request/cannot-download-directory, and a path that does not exist gets 404 with the type not-found.

Sent with Accept-Language: es, as the Ruby SDK does after Files.language = "es", GET /api/rest/v1/file_actions/metadata/{path} for a path that does not exist answers with the message a Files.com site sent in Spanish, No se ha encontrado. Esto puede estar relacionado con tus permisos., and the same status, title and type. That is the simulator's only translation: any other Accept-Language value, and every other operation and error, gets the English message.

GET on the download_uri returns 200 with the file's bytes, Content-Length, Content-Type: application/octet-stream, Accept-Ranges: bytes and an ETag. With a Range header naming one byte range (bytes=7-99, bytes=7- or bytes=-100), it returns 206 with just those bytes and a Content-Range header. A range that ends past the end of the file is shortened to it, and one that starts past the end gets 416 with Content-Range: bytes */{size}. Any other Range value, such as several ranges, is ignored and the whole file is sent, as HTTP allows; so is every range on an empty file. HEAD is not simulated.

A download URL names one version of a file. Once the file is replaced or deleted, its old download URLs get 409 with the type download_source_changed, and the Go SDK then requests a new URL. A download already in progress finishes with the bytes it started with.

Download request status

Files Sync Worker, the storage proxy behind some Files.com download URLs, gave each download it began streaming an X-Files-Download-Request-Id header (with the transfer's X-Files-Download-Id) and answered GET on the download URL joined with that ID with the request's status (its GET /download/:code/:request_id at commit 9073d2bc). The Go SDK was written against it: it joins the ID to the URL (url.JoinPath) to explain a failed download or to confirm a file whose size it could not trust. A reset selects this protocol with download.request_status (see Transfer profiles); without it, downloads carry neither header and that path is not simulated (501), as before. Whether any Files.com deployment still serves this protocol is not established.

With it selected, each 200 or 206 storage download response gets a new ID. A response that fails before it streams gets none, as the captured producer source gives none to such a response; what a response that failed after it began streaming carried is not established. header_on_failure: true gives a failed response an ID too; that is the simulator's choice, for reaching a client's handling of an ID on a failed response. With header_on_failure: false (the default), a fault's own answer gets no ID. With it set to true, non-2xx fault answers, including expired_url and redirects, get an ID; html_page returns 200 and still gets none.

The ID answers the status the reset chose, completed by default; the simulator does not run a transfer lifecycle. A request it holds answers 200 in the producer's envelope, with http_code spelled with an underscore and its null members kept:

{"type": null, "title": null, "error": null, "http_code": 200, "errors": null,
 "data": {"file_download_id": "…", "request_id": "…", "id": "…", "type": "file_download",
          "bytes_transferred": 6, "file_transfer_id": "…", "status": "completed", "method": "get"}}
status type and error bytes_transferred
completed null The bytes of the stored version the response was made from: the whole file, or the range.
started null null, as for a request still streaming.
failed, error download_error (the producer's fallback type) and a message saying the profile chose the status As for completed; null for an ID that header_on_failure gave a failed response.

A request the simulator does not hold, and a download URL it does not hold, get the producer's 404: type download_request_not_found, error "Download request expired or not found. Please try this transfer again.", http_code 404, and data naming the URL's download and the request found by its ID (null when none is). A request is not held when its ID was never issued, is malformed, is another simulator's, is from before the latest reset (the producer lost its requests when it restarted), or was displaced by newer requests. A download URL is held only when files.download issued it in this reset, as the producer held the transfer each download URL named: a well-formed URL for a version never made, or for one whose URL was never asked for, is not held. An issued URL stays held when its file is replaced. A reset keeps the newest FILES_MOCK_MAX_RECORDS requests and the newest FILES_MOCK_MAX_RECORDS issued URLs, and downloads go on after that. As the producer found a request by its ID alone, an ID also answers at another held download URL of the reset, and its data then names both (file_download_id and file_transfer_id differ). The query after the joined path is not checked, as the producer skipped its expiry check for a status lookup; the Go SDK sends the download URL's own. Fault rules may target transfers.download_status like any transfer operation.

bytes_transferred counts what the simulator read from the stored version for the response, as the producer counted what it read from the remote. A truncate, short_body, excess_body or stall rule changes what reaches the client (the journal's sent_bytes), not this count, so compare it with what you received, as the Go SDK does for an untrusted size.

For example, against a server on port 4041 ("status": "failed" instead answers with "type":"download_error" and its message):

curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:4041/__files_mock/v1/reset -H 'Content-Type: application/json' \
  -d '{"profile": {"download": {"request_status": {"status": "completed"}}}, "fixtures": {"files": [{"path": "f.txt", "text": "abcdef"}]}}'   # 200
url=$(curl -s localhost:4041/api/rest/v1/files/f.txt | ruby -rjson -e 'puts JSON.parse($stdin.read)["download_uri"]')
id=$(curl -s -o /dev/null -D - "$url" | awk 'tolower($1) == "x-files-download-request-id:" { print $2 }' | tr -d '\r')
curl -s "$url/$id"; echo   # {"type":null,"title":null,"error":null,"http_code":200,"errors":null,"data":{…,"bytes_transferred":6,…,"status":"completed","method":"get"}}

The status data leaves out what the producer also held: started_at and completed_at, whose wire encoding was not established, and its remote server's log context (site_id, remote_server_id, path, server_type, client_ip). How long the producer kept a request, how its status moved through failures, cancellation and ranges, and which errors it stored are not simulated.

Folders

Folders are records, as on Files.com: POST /api/rest/v1/folders/{path} creates one, it is listed and described like a file (with the type directory and no size), it stays after its last file is deleted, and it is deleted only when it is empty. The root folder always exists; its path is empty.

By default the simulator behaves like a site whose "always create parent folders" setting is on: every write, whether creating a folder, beginning an upload or finalizing one, creates the folders missing above its path, and mkdir_parents has no further effect, even when it is false. The reset profile {"site_policy": {"always_mkdir_parents": false}} turns the setting off, as it is by default on a Files.com site: then creating a folder, beginning an upload or finalizing one whose parent folder is missing gets 404 with the type not-found unless the request sends mkdir_parents: true, which creates the missing folders, as the API's folder lookup does. Unzip creates its destination folder either way, as the API does; a copy or move into missing parent folders gets 501 under that profile, because how Files.com treats it there is not simulated. The readiness response reports the policy in effect (namespace.site_policy). A write that cannot finish creates none of the folders it would have needed.

Creating a folder is exclusive: a name that a file or folder already holds gets 422 with the type processing-failure/destination-exists, and the existing entry is never adopted. A file cannot be a folder's parent (422, bad-request/folder-must-not-be-a-file).

GET /api/rest/v1/folders/{path} lists a folder's files and folders in path order, compared as the API compares paths. per_page must be a whole number from 1 to 10000 and defaults to 1000. When more entries remain, the response carries the same cursor in both the X-Files-Cursor and X-Files-Cursor-Next headers; pass it back as cursor, with the same per_page, for the next page. The last page has neither header. A cursor continues after the last entry it returned, so entries created later appear on later pages when they sort after it, entries deleted before they are reached are skipped, and none is returned twice. A cursor is accepted only for the folder, per_page, simulator process and reset epoch it was issued for; anything else gets 422 with the type bad-request/invalid-cursor. Listing a folder that does not exist gets 404.

with_previews (when the Swagger document declares it) asks for each file's preview, which the simulator does not hold. Folders have no previews, so a page of folders is answered exactly as without it. A page that would hold a file gets 501 with the type simulation/not-supported before a cursor is issued for it; the same request without with_previews, or with it false, lists that page.

DELETE /api/rest/v1/files/{path} deletes one file, or one empty folder, and returns 204. A folder holding anything gets 422 with the type processing-failure/folder-not-empty unless recursive=true, which deletes everything strictly inside it (foo/..., never foo2) and then the folder, releasing the bytes of every file it held. The root is never deleted (422 without recursive, 501 with it). Look the path up afterwards to see that it is gone. The boolean parameters of this operation, GET /files/{path}, GET /file_actions/metadata/{path} and GET /folders/{path} may also be True or False, as .NET writes booleans; this applies to those operations' parameters wherever the request carries them, and to no other operation.

File metadata

PATCH /api/rest/v1/files/{path} sets a file's or folder's custom_metadata, priority_color and provided_mtime, and the file or folder is returned with them. They stay with the path when it is moved and are dropped when a new version replaces the file or the path is deleted. A folder moved or copied onto an existing folder (a merge) leaves the destination's own metadata; a move still ends the source's, so a new entry at the old path starts without it, and a copy keeps it on the source. A path that does not exist gets 404.

An update replaces the whole custom_metadata object, and a null one clears it to {}. Its values must be strings or null: a number, a boolean, an object or an array gets 422 bad-request before the path is looked up, and nothing is converted to text. Empty strings and null values are kept. As the API's model refuses to save it, an object with more than 32 keys, a key longer than 256 characters or a non-null value longer than 1024 (counted in characters, not bytes) gets 422 processing-failure/model-save-error listing every failure in order (model_error_keys {"custom_metadata": ["key_too_long", "value_too_long", "too_many_keys"]} for each). A refused update changes nothing it sent.

Underscore destinations

The SDKs' external-destination helpers upload, copy and move to paths such as _/RemoteServers/7/in/x.bin, _/Snapshots/8/... and _/Sites/9/.... Here they are ordinary paths in the simulator's namespace, and each request's journal entry records the store it names as destination_scope (remote_server, snapshot or child_site, with its ID). No remote server, snapshot or child site is involved.

Copying and moving

POST /api/rest/v1/file_actions/copy/{path} and POST /api/rest/v1/file_actions/move/{path} take destination, and overwrite (both) and structure (copy). They copy or move a file, or a folder with everything strictly inside it, keeping exact bytes and the original spelling of every path, and answer 201 with {"status": "completed"}, as Files.com answers a move it finishes at once. A copy is a new file whose bytes count against FILES_MOCK_MAX_TRANSFER_BYTES; a move keeps the same file, so its old download URLs still send it. Missing parent folders of the destination are created.

  • An existing destination gets 422 with the type processing-failure/destination-exists ("The destination exists.") unless overwrite is true: then files replace files and folders merge into folders. Overwriting a file with a folder or a folder with a file, copying or moving a folder into itself or onto a folder that holds it, copying or moving the root, structure for a file and copy_behaviors: true get 501.
  • A destination sent null or empty is the site root, which always exists. Without overwrite that is 422 processing-failure/destination-exists. With overwrite, Files.com enqueues the action, and the FileMigration it creates fails its destination presence validation; the API answers that with its catch-all 500, {"error": "Internal server error, please contact support or the person who created your account.", "http-code": "500"} and no X-Files-Error-Class. Nothing changes and nothing is enqueued. This follows the Files.com source (its handlers outside tests); it has not been observed on a live site.
  • structure: true copies a folder's folders only.
  • A folder name in the destination (any name but the last) that ends in whitespace gets 422 with the type bad-request/path-cannot-have-trailing-whitespace, as the API's destination rule refuses it; unzip applies the same rule to the folders above its destination folder. A last name ending in whitespace is not refused by that rule.
  • Every check happens before anything changes, including the record and byte limits.

With the reset profile {"file_actions": "pending"}, a copy or move answers {"status": "pending", "file_migration_id": N} instead and changes nothing yet. GET /api/rest/v1/file_migrations/{N} then reports processing the first time it is polled and, the next time, applies the action in one step against the state at that moment: completed with files_moved, or failed with the reason in failure_message and nothing changed. As Files.com presents a FileMigration, files_moved is 0 until the migration has processed anything and files_total is always 0.

ZIP files

When the Swagger document declares them (the local export b8129459 and the production schema do), a stored ZIP file can be listed and extracted as Files.com does it, following the Rails file actions controller and the integration worker's ZIP reader:

  • GET /api/rest/v1/file_actions/zip_list/{path} answers each entry's path and uncompressed size, in the order of the archive's central directory, folder entries (dir/) included. A folder gets 422 bad-request/folders-not-allowed, a missing path 404, and a file the reader cannot read (not a ZIP, an encrypted entry, a broken directory, more than 10000 entries) 422 processing-failure/invalid-zip-file.
  • POST /api/rest/v1/file_actions/unzip takes path (the ZIP file), destination (a folder), and filename and overwrite. A missing or folder path is refused as above, and a path sent null or empty is the site root, a folder, so it gets bad-request/folders-not-allowed before anything is made. The destination folder and its parents are created with the request; a destination sent null or empty is the root folder, which the extraction fills. It always answers 201 with {"status": "pending", "file_migration_id": N}, whatever the file_actions profile, because Files.com extracts in a FileMigration. Polling the migration to completion extracts every file entry (or only filename) to its path under the destination, creating the folders its path names; folder entries create nothing themselves. It fails with Files.com's message, changing nothing, for an entry path that starts with / or has a .. segment ("Invalid ZIP entry path: …"), a filename the archive does not hold ("File not found inside ZIP: …"), an existing file without overwrite ("The destination exists."), an unreadable archive, or a compression method other than stored or deflate. Files.com extracts entries one at a time and may keep those it extracted before one fails; the simulator checks the whole extraction first, which is not a claim about what Files.com keeps.
  • An extraction is admitted against FILES_MOCK_MAX_RECORDS and FILES_MOCK_MAX_TRANSFER_BYTES by the entries' recorded uncompressed sizes before any entry is decompressed; one the limits refuse fails its migration with simulation/limit-exceeded having expanded nothing, and an archive that records more than it holds is counted by what it records. A replaced file's bytes are not credited against the limit. No entry's data grows past its recorded size: decompression stops within one zlib output chunk (16 KiB) of it, and the entry is then not simulated, as below.
  • Not simulated (501): ZIP64 archives, entry names that are neither marked UTF-8 nor plain ASCII, an entry whose data does not match its recorded size, and extracting onto a folder. CRC-32s are not checked, as the worker does not check them.
  • The Go SDK's ZIP batch downloads (/zip_downloads) are not simulated; that route and its consumers belong to the Go/CLI owner.

Saving a ZIP

When the Swagger document declares it (the local export b8129459 does), POST /api/rest/v1/file_actions/zip saves a ZIP of files and folders as a file, following the Rails file actions controller, its ZipDownload and FileMigration#perform_zip, and files-protocol-server's ZIP stream (current repository sources, not a deployed service's identity):

  • It takes paths (the selection), destination (the file to save) and overwrite, and checks them in the controller's order. A paths or destination left out is 422 bad-request (paths is missing), the API's parameter validation; paths sent null, [] or "" (an empty array to the API's coercion) is 422 bad-request/request-params-required, the controller's own check. A destination sent null or empty is the site root: after the selection, its lookups and the no-files check below, the FileMigration fails its destination presence validation, which the API answers with the catch-all 500 described under Copying and moving; no migration is made. Exact duplicates are removed, keeping the first; paths are compared exactly as sent, and a path in another spelling is refused as not simulated (see Differences from the Files.com API). A selected path that does not exist is 404 not-found, checked before the destination. Then comes the destination: a folder name in it (any name but the last) ending in whitespace is 422 bad-request/path-cannot-have-trailing-whitespace, and a destination inside a file is 422 bad-request/folder-must-not-be-a-file. None of these refusals changes anything. Then the destination's missing parent folders are created, and only then must the selection hold a file (an empty file counts; a folder whose tree holds none does not). A selection that does not is 422 processing-failure/model-save-error (Files.com's type and status; its validation message is not modeled). The folders already created stay, as on Files.com, and no migration is made. Otherwise the request answers 201 with {"status": "pending", "file_migration_id": N}, whatever the file_actions profile: accepting the request is not a promise that the ZIP will be saved. The migration's path is the first requested path, not the first in sorted order.
  • Polling the migration to completion reads the selection then: the current version of each selected file and the files each selected folder holds at that moment. It makes the archive from them and saves it as the destination, creating the destination's parent folders again if they were deleted meanwhile. A selected file is named by its own name (a/file.txt becomes file.txt); each file in a selected folder, at any depth, by the folder's name and its path inside it (selecting x/a gives a/...). There are no folder entries, so an empty folder is left out and an empty file kept. The selection is taken in sorted order, and a repeated name is numbered as the ZIP stream numbers it, counting every name the archive has used: file (1).txt, file (2).txt, README (1) for a name without an extension, and file (1) (1).txt for a later file already named file (1).txt. Saving the archive is one processed operation, so a completed ZIP reports files_moved 1, whatever its number of entries (the journal records that number as entries).
  • The destination is checked when the migration runs: an existing file fails it ("The destination exists.") unless overwrite is true, and is then replaced; a file saved there after the request counts too. Files.com deletes the file an overwrite replaces before its ZIP stream starts, so the simulator leaves that old file out of the new archive, even when the destination is inside a selected folder. Separately, the simulator chooses the archive's entries before it saves the new archive, so a new destination inside a selected folder is not one of them either. When Files.com's receiving upload makes a new destination visible, relative to the stream reading the selection, is not established by the sources the simulator follows: that case is the simulator's own behavior, not a claim of parity with Files.com. Files.com streams the archive into the destination and does not promise to keep the old file if that fails; the simulator builds the archive whole first, which is not a claim that replacement is atomic.
  • Each entry is deflated, or stored when that is not smaller, with its CRC-32 and sizes in its local header and its name marked UTF-8. Files.com's compression method and level are deployment settings, so neither they nor the archive's bytes or entry order are a contract; its entry names and contents are. The saved archive lists and extracts as any stored ZIP does (above).
  • A ZIP is admitted against FILES_MOCK_MAX_TRANSFER_BYTES by the most bytes it can take (its files' sizes and headers) before it is built, and against FILES_MOCK_MAX_RECORDS for its file and the parents the request creates; one past a limit fails with simulation/limit-exceeded, saving nothing. It holds at most 10000 entries, what the reader lists.
  • Not simulated (501, or a failed migration with that message): a numbered name that repeats another entry's name, a repeated name the stream cannot number (such as .env, which has a dot and no extension), replacing a folder, and an archive needing ZIP64; and, when the migration runs, a selected path that no longer exists, a selection naming the file its overwrite removes, a selection that no longer holds a file, and a destination that is now inside a file. What Files.com does when its stream cannot read a file after the archive has started (it can end with an incomplete archive), and how that ends the migration, is not established, and neither are remote mounts, workspaces, bundles, permissions, locks or names outside UTF-8.

Transfer profiles

A reset's profile chooses how uploads and downloads behave until the next reset (see Reset and fixtures). The profiles are simulation policies, never claims about a particular real site or provider:

Setting Values Effect
upload.mode legacy (default), serial, parallel legacy keeps the advertisement described above without checking it. serial advertises parallel_parts: false and admits one part of an upload at a time, in order; parallel advertises true and admits parts in any order. Both check the settings below; a violation gets 409 or 422 with the type simulation/profile-violation.
upload.retry_parts true (default), false With false, a second send of a part is refused; restart the upload.
upload.partsize bytes The part size each part is issued with (partsize, and next_partsize for the part after it). It is a hint: parts of other sizes, larger or smaller, are accepted, each within FILES_MOCK_MAX_BODY_BYTES, and parallel_parts: false asks only for one part at a time, in order. Only variable_part_limits bounds part sizes further.
upload.partsize_changes [{"from_part": N, "partsize": bytes}, ...] From part N on (N at least 2, increasing), parts are issued with the new size and next_partsize announces it. Parts already issued keep theirs.
upload.variable_part_limits min_nonfinal_bytes, max_part_bytes, max_parts, max_file_bytes Unequal parts within these limits. An upload begun without size needs its final size and every part's etags to finish.
upload.part_offset_query true, false Parts may send ?part_offset=N: every send of a part must name the same offset, and at finalize the offsets must match the sizes of the parts before.
upload.upload_target_class s3, agent_fiw, generic Advertised as is.
upload.max_concurrent_parts, upload.throttle_retry_after 1–64, seconds Parallel only: at most this many parts hold a place at once across the uploads not yet finalized, each from its admission, before its body is read, until the simulator has applied or refused it and made its response; more get 503 with Retry-After. The journal's upload_parts reports the count.
signed_urls.lifetime seconds Upload and download URLs carry a synthetic AWS SigV4-style presigned query with a sentinel credential and a signature over the exact query; a URL sent with any byte of its query changed gets 403 SignatureDoesNotMatch (XML), as presigned storage URLs do. The lifetime is advertised, not enforced; use expired_url faults to expire URLs. Not combined with part_offset_query.
download.request_status {"status": "completed", "started", "failed" or "error", "header_on_failure": false or true} The historical Files Sync Worker protocol: each successful storage download response carries X-Files-Download-Request-Id and X-Files-Download-Id (a failed one too with header_on_failure, which the captured producer source does not do for a response that fails before it streams), and the download URL joined with the ID answers the chosen status in the producer's envelope; see Download request status. Absent by default.
download.size sent (default), withheld With withheld, a whole-file 200 has no Content-Length (Puma sends it chunked to an HTTP/1.1 client), as the historical producer answered for a remote whose size cannot be trusted, and a 206 has Content-Range: bytes a-b/* (the simulator's choice; the producer's range header for that case was not captured). Clients such as the Go SDK then treat the size as untrusted.
download.identity absent (default), contract_v1 With contract_v1, GET /api/rest/v1/files/{path} also takes with_download_identity (which may be True or False, as the Python SDK sends it) and expected_download_identity and returns download_identity (version, token, size, required_headers with a strong If-Match), following the adopted download identity contract. The URL then requires that If-Match; a changed file, even one of the same size, gets 412 download_source_changed, and renewing with the token returns the same identity while the file is unchanged. A token that is not fdi1. followed by URL-safe Base64 of at most 4096 bytes gets 400; one that names no current version (a changed file, or one from before a reset) gets 422 processing-failure/download-identity-unavailable. The local Rails export (b8129459) declares both parameters and the production pin does not: from a schema that declares them, absent acts as a site with the setting off (with_download_identity gets an ordinary download, and an expected token gets 422).

variable_part_limits, part_offset_query and upload_target_class are FileUploadPart fields of the combined adaptive schema; a server generated from a schema without them refuses a profile that sets them.

Separately from that advertised hint, each part has an intended length: its actual length, fixed the first moment it is known, from a declared Content-Length or the bytes a first complete send delivered. Every later send of the part, a retry after a fault or a send to a renewed URL, must hold exactly that many bytes (409 otherwise). A send whose length is unknown (no Content-Length) is measured once read, and one that ends early (an aborted body) binds nothing and stores nothing.

Upload and download URLs

The upload_uri and download_uri values are URLs under /__files_mock/transfer/. Use them as they are: they are not Files.com API paths, and they work only on the simulator process that issued them, until its next reset. An upload URL works until the upload is finalized; its expires time, 15 minutes after it was issued, is not enforced.

The URLs start with the address and port the request arrived on, such as http://127.0.0.1:4041. They never reflect the request's Host header. When your tests reach the server at a different address, such as a port published from a Docker container, set FILES_MOCK_TRANSFER_ORIGIN to that origin, for example http://127.0.0.1:40410. The server refuses to start if it is not an http or https origin without a path.

Some SDK download helpers accept only https URLs. Behind a TLS bind the URLs use https:

FILES_MOCK_MODE=simulation bundle exec puma -b 'ssl://127.0.0.1:4443?key=key.pem&cert=cert.pem&verify_mode=none'

Give your client the certificate as a trusted CA (for Node.js, NODE_EXTRA_CA_CERTS=cert.pem). The certificate must name 127.0.0.1.

Example

Start a simulator that accepts 5 MiB parts:

FILES_MOCK_MODE=simulation FILES_MOCK_MAX_BODY_BYTES=8388608 bundle exec puma

With the Go SDK:

config := files.Config{APIKey: "placeholder", EndpointOverride: "http://127.0.0.1:4041"}.Init()
client := &file.Client{Config: config}
if err := client.Upload(file.UploadWithFile("report.pdf"), file.UploadWithDestinationPath("reports/report.pdf")); err != nil {
	log.Fatal(err)
}
if _, err := client.DownloadToFile(files.FileDownloadParams{Path: "reports/report.pdf"}, "report-copy.pdf"); err != nil {
	log.Fatal(err)
}

With the Python SDK:

import files_sdk

files_sdk.base_url = "http://127.0.0.1:4041"
files_sdk.set_api_key("placeholder")
files_sdk.file.upload_file("report.pdf", "reports/report.pdf")
files_sdk.file.download_file("reports/report.pdf", "report-copy.pdf")

The same steps with curl and jq:

API=http://127.0.0.1:4041/api/rest/v1
PART=$(curl -s -X POST $API/file_actions/begin_upload/hello.txt -H 'Content-Type: application/json' -d '{}')
REF=$(jq -r '.[0].ref' <<<"$PART")
ETAG=$(curl -s -D - -o /dev/null -X PUT --data-binary 'hello, world' "$(jq -r '.[0].upload_uri' <<<"$PART")" |
  awk 'tolower($1) == "etag:" { gsub(/[\r"]/, "", $2); print $2 }')
curl -s -X POST $API/files/hello.txt -H 'Content-Type: application/json' \
  -d "{\"action\": \"end\", \"ref\": \"$REF\", \"etags\": [{\"etag\": \"$ETAG\", \"part\": 1}]}" | jq -c .
curl -s -r 7-11 "$(curl -s $API/files/hello.txt | jq -r .download_uri)"; echo   # world

Differences from the Files.com API

  • Paths are stored exactly as sent, but the Files.com API compares them with its comparison map, so café.bin and CAFE.bin are one file there. Rather than store an entry the API would not, the simulator returns 501 for a new path that compares equal to an existing file or folder spelled differently, including as a parent folder. The same spelling still replaces its own file. Beginning an upload where a folder is, and a path two or more levels inside a file, also get 501.
  • The API rejects a path with a name that, compared that way, is empty, . or .., or contains a slash, such as .. (two fullwidth dots) or a℀b. The simulator returns 501 for these instead of the API's error, and also for paths the API would rewrite: with a leading, trailing or repeated slash, a . or .. segment, or a backslash. The API's request-path rules (see Files) and its destination rule (see Copying and moving) are checked; its other path rules, such as length limits, are not, so a path accepted here may still be refused by the API. Styles and the lists scoped by path (see Lists scoped by path or owner) do not apply the request-path rules, since the Files.com source read for this simulator does not establish that their actions do. A path parameter that disagrees with the path in the URL gets 422.
  • When a request breaks several parameter rules at once, the API reports all of them in one message. The simulator joins the messages for several invalid values, but reports only the first parameter left out, and none left out beside an invalid value.
  • The upload profile is advertised, not enforced: parts sent at the same time or out of order are accepted. size is optional, as it is in the SDKs' own known-size uploads, and is checked only when it is sent. The simulator does not model or certify uploads of unknown size, adaptive part sizes or parallel parts, even where it accepts such requests.
  • The simulator checks that every uploaded part is listed, that a sent size matches the parts, and, without etags, that the received parts are numbered 1 to N. These are the simulator's own safeguards: the Files.com API may accept such requests, so a rejection here does not prove the API would reject them. Finalizing without etags is simulated only for a sequential upload of known size on Files.com's own storage; remote server mounts and their providers, whose part rules differ, are not simulated.
  • with_direct_connection_info is accepted, and no direct connection information is returned, which the API also allows.
  • These requests get 501: begin_upload with restart, with_rename or buffered_upload, or with a ref but no part; POST /api/rest/v1/files/{path} with an action other than end, or with custom_metadata, length, part, parts, restart, copy_behaviors, structure, with_rename or buffered_upload; GET /api/rest/v1/files/{path} or GET /api/rest/v1/file_actions/metadata/{path} with preview_size, with_previews or with_priority_color, or with an action other than stat; GET /api/rest/v1/folders/{path} with any parameter other than cursor and per_page (such as sort_by, search or type), or for a file's path; and a recursive delete of the root.
  • Files and folders do not report who created them, permissions or locks, and checksums such as md5 are not returned. Listings do not return X-Files-Cursor-Prev.

Locks

Locks are advisory, as in the Files.com API: a lock never stops a file being written, moved or deleted, and it stays at the path it was made for when the file moves or goes away. Clients check locks themselves. The simulator implements the public list, create and delete operations from the Files.com lock controller and model as read in their source, not as observed on a live site.

Creating. POST /api/rest/v1/locks/{path} creates a lock only when allow_access_by_any_user and exclusive are both true (JSON booleans, or the strings "true" and "false", as a form body sends them). Any other combination, including a missing or null flag, gets 422 bad-request/request-params-required with the message Required request parameter missing: token, allow_access_by_any_user, or exclusive: the API accepts other combinations only with a token, type or scope, which are internal parameters the public schema does not declare. A value of the wrong type is refused first, with 422 bad-request (exclusive is invalid), and the flags are checked before the path and any conflict. A lock applies to its subfolders unless recursive is false, and lasts timeout seconds: 43200 (12 hours) when it is missing, zero or negative, and at most 604800 (one week). The answer is 201 and the Lock:

{ "path": "docs/report.txt", "timeout": 60, "depth": "infinity", "recursive": true, "scope": "exclusive", "exclusive": true,
  "token": "0c4c8f5e-6b0a-4f3e-9a55-3d1f2b8e7c11", "type": "office", "allow_access_by_any_user": true, "user_id": -1 }

depth is "0" for a lock that does not apply to subfolders. The token is a new UUID. user_id is -1 because the simulator authenticates nobody (as with a site-wide API key), so username is left out, and so is owner, which only an internal parameter sets. The path is kept exactly as sent and must be in the form the API keeps paths in (see Differences from the Files.com API). It follows the API's path rules for a file's path (see Files): the lock's own name may end in whitespace, the folders above it may not, and a zero-width space is refused. It need not name a file or folder.

How locks are found. The simulator finds locks the way the model does: it keeps each lock as PATH:ID in a sorted index and reads the index by byte prefix, not by folder. The locks at a path are those whose entry starts with PATH:, so a lock at a:b is also at a. The range from a path holds every entry that starts with the path: the path's own locks, the locks inside it at any depth, and those at names that only start with it, such as a2 and a2/x for a. Each folder above a path is looked up at its own name, so a2 is never above a/x. Entries come in index order: by PATH: byte by byte (a/b before a, since / sorts before :), and in creation order within a path.

Conflicts. Every lock the public API can create is exclusive, so a new one conflicts with every lock the model's validation compares it with. In order, these are:

  1. its siblings, the live locks at its path;
  2. every lock at each folder above it that applies to subfolders, nearest folder first;
  3. when the new lock applies to its own subfolders, everything in its range: the locks inside it and at names that only start with it, and its own path's locks a second time.

The answer is 422 processing-failure/resource-locked, whose message lists them as the model does: exclusive lock 1f0e… at /docs, exclusive lock 9a2c… at /docs/a.txt, exclusive lock 1f0e… at /docs. So creating a2 and then a lock at a that applies to subfolders is refused, while one at a that does not apply to subfolders is created. A lock above that does not apply to subfolders never conflicts.

Listing. GET /api/rest/v1/locks/{path} answers the live locks at the path and those at each folder above it that apply to their subfolders, sorted by path. With include_children=true it answers the path's range instead of its own locks, no more than one level deeper than the path. For a, that means a, a/b, a2 and a2/x, but not a/b/c. The action answers this whole array: cursor (a string) and per_page (a whole number, any value) are checked as declared and change nothing, and no cursor comes back.

Releasing. DELETE /api/rest/v1/locks/{path}?token=… answers 204 for the lock with that token at the path. When several match, the one with the latest deadline is released, provided it has not expired. Any other token or path, an expired lock, a lock released already and a lock from before a reset get 404 not-found. A token left out gets 422 bad-request (token is missing), as the API's parameter validation answers it; one sent null or empty passes that validation and, after the path rules, finds no lock (404). Every lock the public API can create allows any user of the site to release it, and the simulator is one synthetic site: a lock made with one API key is listed and released with any other.

Time. The lock clock is whole seconds, 0 at every reset, and moves only when a test advances it (POST /__files_mock/v1/locks/clock), so expiry never depends on how fast tests run. A lock expires once its deadline (its creation time plus its timeout) is before the clock; at a deadline equal to the clock it is still live. An expired lock is not listed, not releasable and not a sibling of a new lock, but it is still held. The model's queries for the folders above a path and for a range keep expired locks, so an expired lock goes on conflicting in two cases:

  • as a lock above a new lock, when it applies to subfolders;
  • as anything in a recursive new lock's range, including a lock at that very path.

A new lock at the same path that does not apply to subfolders is created. This lasts until POST /__files_mock/v1/locks/cleanup removes every expired lock, as the model's removal job does when it runs. Nothing else removes an expired lock before the next reset.

Held locks, expired ones included until a cleanup, count against FILES_MOCK_MAX_RECORDS; past it a create gets 409 simulation/limit-exceeded. token, type, scope, depth, owner and bundle_registration_code, which the API takes but the public schema does not declare, get 501: they change what the request does (a token refreshes an existing lock), so they are refused rather than ignored.

API=http://127.0.0.1:4041/api/rest/v1
TOKEN=$(curl -s -X POST $API/locks/docs/report.txt -H 'Content-Type: application/json' \
  -d '{"allow_access_by_any_user": true, "exclusive": true, "timeout": 60}' | jq -r .token)
curl -s "$API/locks/docs?include_children=true" | jq -c 'map(.path)'          # ["docs/report.txt"]
curl -s -X POST http://127.0.0.1:4041/__files_mock/v1/locks/clock -H 'Content-Type: application/json' -d '{"advance_seconds": 61}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE "$API/locks/docs/report.txt?token=$TOKEN"   # 404: expired

What the lock simulation does not establish

The contract comes from the Files.com source (the lock controller and FileLock model), not from a live site. These points stay open or differ:

  • Who may act on another user's lock (site administrators, the lock's user, a parent site's users) and write permission are not simulated. Without write permission the API answers a list with []; the simulator grants every request.
  • The model's lock IDs are not simulated, so the order of several locks at one path, in a conflict message or a list, is the simulator's creation order.
  • The model's removal job selects locks by the second of their deadline, so it can remove a lock during its last second; the simulator's cleanup removes only expired locks. How often the job runs is not known and is not simulated.
  • Refreshing a lock needs its internal token, so it is not simulated. The source also computes a refreshed deadline from the timeout as given, not as limited to one week or defaulted, so a refresh and a create treat out-of-range timeouts differently.
  • Whether the API's model validation (422 processing-failure/model-save-error) can be reached with public parameters is not established; the simulator never answers it.
  • Two exclusive creates at the same moment cannot both succeed here, since every request is applied under one lock; whether the API's creates are atomic is not established. Paths are compared exactly, including case, as the model's index stores them.
  • The source reads the API's own list presentation, not its middleware; that no layer outside the action pages this list is not established.

Control endpoints

Tests control the simulator directly through endpoints under /__files_mock/v1. These requests are never counted as API traffic. POST requests must send Content-Type: application/json.

Readiness: GET /__files_mock/v1/ready

Poll this until it answers 200, with a time limit, before running tests. It identifies the server and what it simulates:

{
  "status": "ready",
  "mode": "simulation",
  "contract_version": 3,
  "simulator_version": "1.0",
  "schema_sha256": "8931ce67…",
  "instance": "f4e868240921",
  "epoch": 0,
  "operations": [
    { "id": "users.create", "method": "POST", "path": "/api/rest/v1/users", "swagger_operation_id": "PostUsers" },
    { "id": "groups.create", "method": "POST", "path": "/api/rest/v1/groups", "swagger_operation_id": "PostGroups" }
  ],
  "records": { "resources": 104, "singletons": 1, "unset_fields": "omitted", "write_only": "password|passphrase|secret|…", "filtering_and_sorting": false, "uniqueness": false, "foreign_keys": false },
  "transfers": {
    "operations": [ { "id": "transfers.upload_part", "method": "PUT" }, { "id": "transfers.download", "method": "GET" } ],
    "origin": null,
    "upload_parts": { "http_method": "PUT", "parallel_parts": false, "retry_parts": true, "partsize": 1048576 },
    "max_uploads": 64, "max_parts": 64, "finalize_etags": [ "listed", "omitted" ],
    "file_actions": { "operations": [ "files.copy", "files.move" ], "overwrite": "files replace files and folders merge into folders", "structure": true, "copy_behaviors": false, "migration_statuses": [ "pending", "processing", "completed", "failed" ] },
    "zip": { "operations": [ "files.zip_list", "files.unzip", "files.zip" ], "zip_creation": true, "methods": [ "stored", "deflate" ], "entry_names": [ "utf-8-flagged", "ascii" ], "zip64": false, "crc_checked": false, "unzip_migration": "pending, applied when polled; the whole extraction is checked before any change", "zip_migration": "pending, made when polled from the files at that moment; the destination is checked then" },
    "download_request_status": { "operation": { "id": "transfers.download_status", "method": "GET", "path": "/__files_mock/transfer/download/{version}/{request_id}" }, "headers": [ "X-Files-Download-Request-Id", "X-Files-Download-Id" ], "selected": null, "size": "sent", "issued": 0, "held": 0, "transfers_held": 0, "max_held": 1000 },
    "state": { "uploads": 0, "files": 0, "bytes_in_use": 0 }
  },
  "namespace": {
    "site_policy": { "always_mkdir_parents": true },
    "listing": { "order": "path", "default_per_page": 1000, "max_per_page": 10000, "max_cursors": 1000, "next_cursor_headers": [ "X-Files-Cursor", "X-Files-Cursor-Next" ] },
    "delete": { "recursive": true, "root": false },
    "state": { "files": 0, "folders": 0, "cursors": 0 }
  },
  "locks": { "clock_seconds": 0, "held": 0, "expired": 0, "max_held": 1000, "default_timeout": 43200, "max_timeout": 604800, "creatable": { "allow_access_by_any_user": true, "exclusive": true } },
  "real_only": [ "authentication", "api key and site identity", "workspaces", "permissions", "lock agency", "behaviors", "creator ids" ],
  "fixtures": [ "users", "action_logs", "…", "site", "folders", "files" ],
  "profile": { "file_actions": "completed", "upload": { "mode": "legacy", "retry_parts": true, "partsize": 1048576, "part_offset_query": false }, "download": { "identity": "absent" }, "list_cursors": "opaque", "site_policy": { "always_mkdir_parents": true } },
  "profile_choices": { "file_actions": [ "completed", "pending" ], "list_cursors": [ "opaque", "special-characters" ], "upload": { "mode": [ "legacy", "serial", "parallel" ], "upload_target_class": [ "s3", "agent_fiw", "generic" ] }, "download": { "identity": [ "absent", "contract_v1" ], "request_status": { "status": [ "completed", "started", "failed", "error" ], "header_on_failure": [ false, true ] }, "size": [ "sent", "withheld" ] }, "site_policy": { "always_mkdir_parents": [ true, false ] } },
  "faults": {
    "match": { "users.list": "continuation", "users.update": "id", "files.copy": "path", "transfers.upload_part": [ "path", "part" ] },
    "statuses": [ 400, 401, 403, 404, 408, 409, 412, 422, 423, 429, 500, 502, 503, 504 ],
    "retryable_statuses": [ 408, 429, 500, 502, 503, 504 ], "permanent_statuses": [ 400, 401, 403, 404, 409, 412, 422, 423 ],
    "kinds": [ "error", "unstructured", "redirect", "expired_url", "html_page", "empty_page", "delay", "hold", "drop_before", "drop_after", "truncate", "short_body", "excess_body", "stall" ],
    "connection_kinds": [ "drop_before", "drop_after", "truncate", "short_body", "excess_body", "stall" ],
    "random": { "session_key": "session", "max_keys": 10000, "max_schedule": 1000 },
    "max_attempt": 100, "max_retry_after": 60, "max_delay_ms": 10000, "max_hold_ms": 120000, "max_holds": 2, "max_rules": 100
  },
  "pagination": { "order": "id", "default_per_page": 1000, "max_per_page": 10000, "next_cursor_headers": [ "X-Files-Cursor", "X-Files-Cursor-Next" ] },
  "limits": { "max_records": 1000, "max_journal_entries": 10000, "max_body_bytes": 1048576, "max_transfer_bytes": 33554432 },
  "state": { "users": 0, "records": 0, "singletons": 0, "file_migrations": 0, "journal_entries": 0, "journal_complete": true, "pending_faults": 0 }
}

(Shortened.) contract_version changes when this control or simulation contract changes incompatibly. schema_sha256 identifies the API schema subset the simulator validates against (lib/simulation/schema.json, which the generator renders from the Swagger document; beside it, lib/simulation/generation.json records that document's name and SHA-256, the schema subset's SHA-256, the reported version, the generator checkout's commit and whether its generator and mock sources were clean, and the command; these are the generator's own statements). instance identifies the server process: a new random value (12 lowercase hex characters) for every process, unless FILES_MOCK_INSTANCE is set when it starts, which lets whoever starts the server recognize it here (as with-scenario.rb does with a new random value for each run). FILES_MOCK_INSTANCE must be 12 lowercase hex characters; any other value stops startup with an error, and an empty value counts as unset. The instance is not a credential: any process of the same user can read it from the server's environment, and the simulator still accepts any credential. epoch counts resets. records summarizes the record resources and what they do not check. transfers lists the byte transfers to issued URLs, the current upload advertisement and transfer limits, the FILES_MOCK_TRANSFER_ORIGIN setting (null when URLs use the address each request arrived on), how an upload may be finalized (finalize_etags), how copies and moves behave, the download request status route and headers with the reset's selection (null when none), size setting, the IDs issued since the reset, the requests and issued download URLs held, and how many of each are allowed (see Download request status), and the unfinished uploads, files and file bytes held now. namespace gives the simulated site policy for parent folders, how folders are listed and deleted, and the files, folders and listing cursors held now. locks gives the lock clock, the locks held (expired ones included until a cleanup) and the timeouts a lock may have, or is null when the Swagger document declares no locks (see Locks). profile is the current reset's profile and profile_choices the values it may take. real_only names what only a real Files.com site can answer: the simulator accepts any credential and reports no identity, permissions, lock agency (who may act on another user's lock), behaviors or creators, so their absence here is never evidence about a real site. contract_version 2 added folders, deletes and finalizing without etags; 3 added record resources for every regular resource, the site singleton, copies, moves, recursive deletes and file migrations, file and folder fixtures, profiles, fault kinds and random faults, and the inventory.

Reset and fixtures: POST /__files_mock/v1/reset

Replaces all state at once: records and their ID counters, uploads, files, folders, listing cursors, file migrations, locks and the lock clock, fault rules, the journal and the profile. fixtures may fill any record resource (an array of records, checked like its create, or like its fields when it has no create), the site (one object), folders (an array of paths) and files (an array of {"path", "text" or "base64", "provided_mtime"}, loaded last). Records get IDs from 1 in the order given, so sending the same fixtures always produces the same records and IDs. profile sets the transfer profiles and file_actions; leaving it out restores the defaults.

curl -X POST http://127.0.0.1:4041/__files_mock/v1/reset \
  -H 'Content-Type: application/json' \
  -d '{"fixtures": {"users": [{"username": "alice"}, {"username": "bob", "ssl_required": "always_require"}]}}'
{ "epoch": 1, "users": [ 1, 2 ] }

A fuller example, with a group, a folder, a file and a serial upload profile:

curl -X POST http://127.0.0.1:4041/__files_mock/v1/reset -H 'Content-Type: application/json' -d '{
  "profile": { "upload": { "mode": "serial", "partsize": 5242880, "retry_parts": true } },
  "fixtures": { "groups": [ { "name": "admins" } ], "folders": [ "inbox" ], "files": [ { "path": "inbox/hello.txt", "text": "hello" } ] }
}'

Send {} to reset to an empty simulator. If any fixture is invalid or over a limit, the reset is refused and the previous state is kept. After a reset, cursors, upload refs, and upload and download URLs issued before it are rejected, and a request the simulator was already handling when the reset happened, including a part body it was still receiving, is refused with 409 (simulation/stale-request) instead of being applied to the new state. A download already being sent finishes. Reset when your test's own requests have finished.

Faults: POST /__files_mock/v1/faults and GET /__files_mock/v1/faults

An explicit fault rule makes one future request fail, and it is used exactly once. A random rule decides every matching request from its seed (see below). A request is matched after its body has been read and parsed, and before its parameters are validated or anything changes. A request refused while being read (a body that is too large, not JSON, or not valid JSON) never counts toward a rule. A request with invalid parameters does count, and gets the fault instead of the 422.

Field Required Meaning
operation yes A simulated operation, for example users.update or groups.find.
kind no What the rule does (default error); see the table below.
status for error, unstructured One of faults.statuses in readiness. For redirect, 301, 302, 303, 307 (default) or 308.
type, message no For error: a Files.com error type such as not-found or bad-request/invalid-cursor, and its message, instead of simulation/injected-fault.
data no For error: a JSON object of at most 4096 bytes sent as the error's data, such as {"host": "127.0.0.1:4043"} with the type not-authenticated/lockout-region-mismatch, so a client's region reroute can be checked against a local answer.
match no Limit the rule to one record: {"id": 2} for users.find, users.update and users.delete, or {"username": "alice"} for users.create. List rules may match {"continuation": true} (a request that sends a cursor) or false, and folder listings also path; record operations on /{name}/{id} match {"id": N}, except those of a record whose id is a string, which take no match. File operations and downloads match on the file's path, and transfers.upload_part on path, part or both, as in {"path": "reports/report.pdf", "part": 2}.
attempt no Fail the Nth matching request after the rule is added (1 to 100, default 1).
retry_after no Seconds to send in a Retry-After header (0 to 60), for error and unstructured.
delay_ms, when for delay, stall, hold Milliseconds (1 to 10000; for hold, 1 to 120000); for delay, before (default) or after the request is applied.
bytes for body kinds Where a body is cut, how much is left off or added, or where it pauses.
location for redirect The origin to redirect to, such as http://127.0.0.1:4042.
cursor for empty_page advance (default) or repeat.
random no {"seed", "rate", "key", "max_faults"}; makes the rule random (then attempt does not apply).
Kind What happens
error A Files.com API error body with the status, and an x-files-mock-fault header naming the rule. Nothing is applied.
unstructured An HTML error page with the status, as a proxy or load balancer answers. Nothing is applied.
redirect The status with Location set to the rule's origin followed by the request's path and query. Another simulator on that origin journals which identity headers arrived.
expired_url For transfers: 403 with the XML Request has expired, as an expired presigned URL answers.
html_page For downloads: 200 with a web page and X-Files-Frontend-App: true instead of the file's bytes.
empty_page For lists: 200 with no items and a cursor that continues from the same place (advance) or the cursor the request sent (repeat).
delay Holds the request before it is applied (other requests go on) or holds its response after.
hold Holds the request before it is applied, for up to 120 seconds: long enough to pass a client's default read timeout (the SDKs' is 80 seconds). It ends early, and the request is then applied at once, when the client closes its connection or sends anything more, as a client that gave up does; a reset ends it too, and the request is then refused as stale. Only explicit rules hold (random does not apply), and at most two hold rules may be pending or holding a request at once: a rule keeps its place from when it is added until the request it selected has been applied or refused (a reset, a closed connection or an error included), so held requests never occupy every server thread. GET /__files_mock/v1/faults gives the requests being held now as holding, a reset's replaced state included until they end. The journal records hold_ms, held_ms and hold_ended (elapsed, client-closed or reset).
drop_before Closes the connection without a response; nothing is applied.
drop_after Applies the request, then closes the connection without a response. A client cannot tell this from drop_before; only the state shows it.
truncate Sends the declared Content-Length but only bytes bytes of the body, then closes.
short_body Sends no Content-Length and leaves the last bytes bytes off, then closes.
excess_body Sends no Content-Length and adds bytes extra bytes, then closes.
stall Sends the headers and bytes bytes, pauses delay_ms, then sends the rest.

Connection kinds (drop_before through stall) write to the connection themselves, so they need a server that supports Rack hijacking, such as bundle exec puma; elsewhere the request gets 501 and the rule stays pending. They work over TLS binds too. The journal records how a response was delivered (fault_kind, sent_bytes, body_bytes, paused_at, delay_ms).

A random rule decides each matching request from its seed, its rate (greater than 0, at most 1), the request's values for the rule's key (any of the operation's match keys, and session, the credential the request carries) and how many earlier matching requests had those values. Requests with other key values never change a decision, so the same requests per key get the same decisions in any interleaving, and adding the same rule after another reset or to another simulator replays which requests it selects. What happens to a selected request does depend on what else arrived: explicit rules come first (the random rule records overridden), a connection fault the server cannot make is unavailable, among random rules the first one added that selects a request fails it, and max_faults is a budget for the whole rule, so once it has failed that many requests it records later selections as budget-exhausted. Which selected requests the budget goes to therefore follows arrival order. GET /__files_mock/v1/faults lists every decision (selected, and the outcome fault, pass, overridden, unavailable or budget-exhausted) with the key values and attempt number, and counts each outcome; sessions appear only by number. List operations match continuation (true for a request that sends a cursor), so a rule can fail only later pages.

curl -X POST http://127.0.0.1:4041/__files_mock/v1/faults -H 'Content-Type: application/json' \
  -d '{"operation": "transfers.upload_part", "status": 503, "retry_after": 0, "random": {"seed": "run-42", "rate": 0.25, "key": ["path", "part"]}}'

For example, to make the first update of user 2 fail once:

curl -X POST http://127.0.0.1:4041/__files_mock/v1/faults \
  -H 'Content-Type: application/json' \
  -d '{"operation": "users.update", "match": {"id": 2}, "status": 503, "retry_after": 1}'

The next PATCH /api/rest/v1/users/2 returns:

{ "error": "Simulated 503 response from fault rule 1", "http-code": 503, "title": "Service Unavailable", "type": "simulation/injected-fault" }

Requests for other operations or other records never use the rule, even when they arrive at the same time. A new rule that could match the same requests as a rule that is still pending is refused with 409.

A fault stores and publishes nothing, so a test can check an SDK's own retry. To make the first attempt at part 2 of an upload fail once:

curl -X POST http://127.0.0.1:4041/__files_mock/v1/faults \
  -H 'Content-Type: application/json' \
  -d '{"operation": "transfers.upload_part", "match": {"path": "reports/report.pdf", "part": 2}, "status": 503, "retry_after": 0}'

GET /__files_mock/v1/faults lists every rule since the last reset as pending or consumed, with matched_requests and the journal consumed_by_request sequence number. Check that the faults your test added were consumed; a pending rule means the failure never happened.

Journal: GET /__files_mock/v1/journal

Lists the API requests since the last reset, oldest first:

{
  "epoch": 1,
  "entries": [
    { "seq": 2, "epoch": 1, "method": "PATCH", "path": "/api/rest/v1/users/2", "operation": "users.update", "id": 2, "fault_id": 1, "status": 503 }
  ],
  "limit": 10000,
  "dropped": 0,
  "complete": true,
  "upload_parts": { "in_flight": 0, "most_in_flight": 0 }
}

upload_parts counts places, not entries: in_flight is the upload parts holding a place now, and most_in_flight the most that held one at once since the last reset. The simulator counts a place under its lock at the moment it admits a part, before the part's body is read, across every upload not yet finalized (the count upload.max_concurrent_parts limits). It gives the place back once it has applied or refused the part and made its response, or as soon as the part's body cannot be read. That is not an acknowledgement that the response reached the client. A reset starts both at 0; a part admitted before a reset is not counted in the state that replaced it, and a part of an upload that is finalized while it is being sent no longer counts.

Entries record the operation (or null for a request that is not simulated), the record ID, the response status and any fault rule used, the JSON type of each request value (wire, such as "string" or "number"), for each array value its element count and the distinct JSON types its elements arrived as, before any conversion (wire_elements, such as {"amounts": {"count": 3, "types": ["string"]}}; a query value is always a string), which identity headers the request carried (credentials: the API key or session by its number in order of first appearance, never its value, and the workspace ID as sent), its user_agent, and arrived_ms and applied_ms, milliseconds since the reset on a monotonic clock, from which request timing and a client's concurrency can be read. A request arrives before it waits for the simulator's lock and, for a part, for a place, so these spans do not show how many parts held a place at once; upload_parts does. File and transfer entries also name the upload, part and file version they used, and bytes and sha256 for the bytes a part stored or a finalize published. A download entry has the length of the response and the version's SHA-256; it is written when the response starts, so it does not show how many bytes the client received. Compare downloaded bytes yourself. Under a download.request_status profile a download entry also has the number of the ID its response carried (download_request), and a transfers.download_status entry the version its URL named, the download_request found by its ID, and how the lookup went (download_request_lookup: found, found_for_other_url, no_transfer for a request found at a download URL not held, evicted, earlier_reset or not_issued). Stat, metadata, delete and folder-create entries give the type found or deleted (file or directory). A folder listing entry has its number of items, and the SHA-256 of the cursor it received (cursor_sha256) and of the one it returned (next_cursor_sha256), so a test can check that each page was requested with exactly the cursor the page before it returned.

A users.list entry also has paging, which describes each paging value by whether it was there (present, by key, whatever the value), its JSON type (wire_type, as in wire, or null when absent) and the SHA-256 of its bytes when it is a string (sha256, the empty string included, else null), never by the value itself:

  • request: per_page and cursor as the request sent them, read from its parsed query and body before an empty_page cursor is resolved or any value is checked. per_page.integer is the value read as a 32-bit whole number, or null when it is missing or is not one (the list still answers 422 for it), and cursor.nonempty is true only for a non-empty string. available is false when the request's parameters could not be read.
  • response: the X-Files-Cursor (cursor) and X-Files-Cursor-Next (cursor_next) headers of the response the simulator selected, each read on its own, after any fault rule chose an error, a redirect or an empty page instead of the page. available is false when no response was selected (drop_before). basis is always selected-rack-response-before-delivery: this is the response the simulator chose to send, not what a client received, since a connection fault may still delay, cut short or drop it.

A missing, unavailable or malformed value proves nothing, and two missing values are not equal. A traversal continued each page when each request's cursor.sha256 equals the previous response's cursor_next.sha256, both non-empty strings, and it ended when an available 200 response has neither header.

Entries never contain request bodies, file content, cursors, query strings, headers or credentials. When the journal is full, later requests still run, but they are counted in dropped and complete becomes false; treat an incomplete journal as missing evidence.

Lock clock and cleanup: POST /__files_mock/v1/locks/clock and POST /__files_mock/v1/locks/cleanup

POST /__files_mock/v1/locks/clock with {"advance_seconds": N}, a whole number from 1 to 2147483647, moves the lock clock forward and answers {"epoch", "clock_seconds"}. POST /__files_mock/v1/locks/cleanup with {} removes every expired lock and answers {"epoch", "removed", "held"}. Any other field in either body gets 400. A server whose Swagger document declares no locks answers both with 404 simulation/unknown-control. See Locks.

Errors from the simulator itself

Status Type Meaning
501 simulation/not-supported The operation, parameter or request body type is not simulated.
409, 413 simulation/limit-exceeded A limit below was reached. Nothing was changed. (Puma's own 413 for an oversized body is plain text; see Limits.)
409 simulation/stale-request The simulator was reset while it was handling the request.
409, 422, 503 simulation/profile-violation A request broke a rule the reset's transfer profile advertises.
any simulation/injected-fault or the rule's type A fault rule you added (with an x-files-mock-fault header).
403 SignatureDoesNotMatch, AccessDenied (XML) A presigned URL sent with a changed or missing query.
404 download_request_not_found (with http_code) A download request status lookup for a request or a download URL the simulator does not hold; see Download request status.
404 simulation/no-inventory The server was generated without an inventory.
400, 409, 415 simulation/invalid-control-request A control request was malformed or conflicts with a pending fault rule.
404 simulation/unknown-control There is no such control endpoint.

Errors use the Files.com API error shape, with error, http-code, title and type fields.

Limits

Simulation mode rejects work over these limits instead of truncating it. Set them with environment variables at startup:

Variable Default Maximum Limits
FILES_MOCK_MAX_RECORDS 1000 100000 Users held at once, and separately files and folders held at once, and locks held at once (expired ones included until a cleanup).
FILES_MOCK_MAX_JOURNAL_ENTRIES 10000 100000 Journal entries kept between resets.
FILES_MOCK_MAX_BODY_BYTES 1048576 16777216 Size of a request body, including each upload part.
FILES_MOCK_MAX_TRANSFER_BYTES 33554432 1073741824 File content held in memory.

At most 100 fault rules can be added between resets, 64 uploads can be unfinished at once, an upload can have up to 64 parts, 1000 folder listing cursors can be issued between resets, 100 distinct credentials can be numbered between resets (a request with a new one after that gets 409 before anything changes; a refused upload part send counts nothing for the part), pending copies and moves keep at most FILES_MOCK_MAX_RECORDS file migrations until the next reset, and a reset keeps the newest FILES_MOCK_MAX_RECORDS download requests and issued download URLs. These limits protect the machine running your tests; they are not Files.com API limits.

The default 1 MiB body limit gets 413 for any larger body, so a client that sends larger parts (the Go SDK sends up to 5 MiB per part, whatever partsize says) cannot finish a bigger upload under it. The transfer profile is the fixed setting for transfers that should succeed: FILES_MOCK_MAX_BODY_BYTES=8388608 (8 MiB), under which the advertised partsize is 5 MiB and a 5 MiB + 257 byte upload fits in two parts without client overrides, within the default FILES_MOCK_MAX_TRANSFER_BYTES. Readiness limits shows which limits a server runs with.

FILES_MOCK_MAX_TRANSFER_BYTES counts all the file content the simulator holds: uploaded parts, files, a part body from the moment the simulator starts reading it, and a replaced file's bytes until a download that started before the replacement finishes. A part body that would pass the limit is refused with 409 before it is read. A reset releases everything except bytes a download in progress is still sending; the transfers.state.bytes_in_use readiness field shows the current count.

A chunked upload part has no declared length, so it reserves FILES_MOCK_MAX_BODY_BYTES until its actual size is known. Near the transfer limit, even a small chunked part can therefore get 409.

Beyond that content, the server holds at most one request body for each request Puma is serving. With the bundled config/puma.rb, Puma reads each body on the thread that serves its request (up to 5 threads unless -t sets more), and connections beyond that wait unread. A body of up to 112 KiB is kept in memory, and a larger one in an unlinked temporary file until the simulator reads it. JSON and form request bodies are then parsed in memory (a JSON number's text stays within its digits plus 32 zeros, so numeric text stays within about seven times the body), and downloads are sent in 64 KiB slices of the stored bytes. The path comparison map, loaded once at startup, adds a fixed amount. Ruby's own memory, including garbage it has not yet collected, comes on top of these, so measure your own setup if memory is tight. A client that sends its body slowly keeps its thread busy until it finishes.

A request body over FILES_MOCK_MAX_BODY_BYTES gets 413 and changes nothing. With the bundled config/puma.rb, Puma refuses it before the simulator sees the request: a declared Content-Length over the limit before any of the body is read, and a chunked body as soon as the received chunks cross the limit. That 413 is Puma's plain-text Payload Too Large response, not the JSON error shape, Puma closes the connection, and the request does not appear in the journal. The simulator checks the same limit itself, so under another server or Puma configuration an oversized body still gets a journaled 413 (simulation/limit-exceeded).

Isolation

Each simulation server keeps its state in its own process memory and shares nothing with other servers, even when they are given the same fixtures. Start one server per test run, on its own port or in its own container, and reset it between scenarios. Simulation mode refuses to start Puma with workers (-w or WEB_CONCURRENCY), because each worker would hold a separate copy of the state.

The simulator has no authentication of its own. It listens on loopback by default, does not send CORS headers, and should only be reachable from the tests that own it.

Shared scenarios

The SDKs and the CLI test common API behavior against this one server. with-scenario.rb runs one test command against a server of its own, loaded with a scenario from scenarios/; each language still makes its own SDK or CLI calls and asserts its own results:

ruby with-scenario.rb --scenario folder-list --out "$OUT" -- go test ./cmd -run TestFolders_ListFor_WithPreviews

Run it with plain ruby, from any directory; the server always uses this directory's Gemfile, even when the caller runs under another project's bundle exec. The launcher:

  1. starts FILES_MOCK_MODE=simulation bundle exec puma on a free port of 127.0.0.1 (never another interface), with a new random FILES_MOCK_INSTANCE for this run that it gives to no one else until the server is ready;
  2. waits for readiness (--startup-timeout, 60 seconds by default) and requires simulation mode, contract version 3, the expected schema_sha256 (--schema-sha256, else FILES_MOCK_SCHEMA_SHA256, else lib/simulation/generation.json) and this run's instance, from a server process that is still running, checked before each attempt and again after the answer;
  3. resets the server with the scenario's reset body;
  4. runs the command, in a process group of its own, without a shell and with no standard input (--consumer-timeout, 900 seconds by default), with the variables below;
  5. then, however the run ended (the command passed, failed or timed out, an error, or INT, TERM or HUP), cleans up in this order: it stops whatever is left of the command's process group, its descendants included; saves the journal and runs the scenario's journal checks, if the server was ready; stops the server's process group; and writes run.json.

Each process group is stopped with TERM, then, after --settle-timeout (15 seconds by default), KILL and the same wait again; only the launcher's two groups are signalled, and a process that left its group (with setsid, for example) is not stopped. A group counts as stopped only when the system reports it has no process left; a group it cannot show to be empty, for example because a check is refused, is recorded as unsettled. Saving the journal is best effort and bounded (2 seconds to connect, 30 to answer): a journal that cannot be read, is incomplete or dropped entries is recorded as missing evidence and never replaces the run's own outcome. After INT, TERM or HUP, the launcher still ends by that signal once it has cleaned up.

The free port is released before the server binds it, so another service on 127.0.0.1 could bind it first. A 200 readiness answer that is not a JSON object, or that reports another instance, mode, contract version or schema, ends the run with 3 at once, as does the server exiting before it is ready: the launcher does not keep polling another service, and neither the reset, the command nor a journal request reaches it. The instance only tells the launcher's own server apart; it is not authentication, and it is not hidden from other processes of the same user.

Variable Value
FILES_MOCK_SERVER_URL http://127.0.0.1:PORT, the only address the test may call
FILES_MOCK_SERVER_HOST, FILES_MOCK_SERVER_PORT The same address in parts
FILES_MOCK_API_KEY A placeholder key for this run: filesmock followed by the letters and digits of RUN_ID, with its ., _ and - dropped. It holds only letters and digits, because SDKs such as Python accept nothing else. Never a real credential
FILES_MOCK_RUN_ID --run-id, or a new UTC time and random suffix
FILES_MOCK_INSTANCE, FILES_MOCK_SCHEMA_SHA256 The running server's instance (the one this run chose) and schema SHA-256
FILES_MOCK_SCENARIO, FILES_MOCK_SCENARIO_FILE The scenario's name and its JSON file, from which a test reads what to expect
FILES_MOCK_OUT The --out directory

--out must be a directory without an earlier journal.json. It receives ready.json, reset.json, journal.json, server.log and run.json. run.json records the run ID and placeholder key; the server (url, instance, schema_sha256, contract_version, log, process_group); the command (argv, status, timed_out, process_group); the journal (complete, dropped, entries) and each journal check's result; setup_error; interrupted (the signal, such as SIGTERM, or the error that ended the run, else null); missing_evidence; and unsettled_process_groups (server, consumer or both). The exit status is the command's when it failed (124 when it timed out, 127 when it could not be started); 3 when the server could not start, was not ready, reported another schema or instance (such as another service answering its port) or refused the scenario (the command is then not run); 4 when the command passed but evidence is missing or a journal check failed; 5 when everything else passed but a process group is unsettled; 2 for a usage error; and 0 otherwise.

A scenario is a JSON object with a name, a description, the reset body, and what its tests need in order to check their own results. journal_checks names checks the launcher runs on the journal; they see only what the server received and never stand in for a test's own assertions. Parallel runs each get their own server, port and run ID.

folder-list holds the folder shared-folder-list with five child folders. Its children are in listing order and per_page is the page size its tests should ask for; the children are folders, so a listing with with_previews is the same. Its check, folders.list cursor chain, follows the listings of shared-folder-list in order (by the journal's cursor SHA-256s): each traversal starts without a cursor and continues with exactly the cursor its previous page returned, a new traversal starts only after the one before it reached its last page, and the last traversal must reach its last page. Several complete traversals pass, so a test should finish every traversal it starts.

A shared scenario adds coverage; it does not replace a language's own tests. The SDKs keep their existing tests, for example Python's malformed, empty, repeated and cyclic cursor, list-context and early-stop tests and the CLI's folder preview test. Once a target's CI job provisions this server from the same schema, its own test entry points (Python's tasks.py, the CLI's test.sh) run the shared scenario as a required test, never one that is normally skipped.

Not yet simulated

GET /__files_mock/v1/inventory lists every operation the simulator does not simulate, with its gap kind and reason: actions whose effects need an owner's simulated contract (running automations and syncs, GPG file actions, incident and partner pairing state), actions that deliver or reach providers (share and welcome email, webhook and SIEM tests, agent and SSO actions), and authentication actions (unlocking a user, resetting two-factor authentication). Operations whose result depends on who is authenticated (sessions, the current API key) are real-only. Authentication, permissions, uniqueness, general filtering and sorting and foreign keys are not simulated either. Requests for these return 501 in simulation mode. Use legacy mode for tests that only need example responses for them.

Checking This Server

./test.sh checks the server's code with its own RuboCop (./lint.sh, which also fails when RuboCop finds no files to check) and runs the server's own tests. The tests start real servers on free loopback ports, and neither step modifies any files. Run bundle install first, or run ./test.sh true to install dependencies and then test.

Before your own tests rely on a running simulation server, read its readiness report (GET /__files_mock/v1/ready): contract_version, schema_sha256 and limits say which server it is and how it was started. GET /__files_mock/v1/inventory lists every operation of its Swagger document and whether and how it is simulated.

Getting Support

The Files.com team is happy to help with any issues you may have running the Files.com mock server.

Just email support@files.com and we'll get the process started.

About

Mock Files.com Server API for your own Integration Testing

Resources

Security policy

Stars

20 stars

Watchers

6 watching

Forks

Releases

Packages

Contributors

Languages