verification
use it
# add to stunt.yaml
services:
photos:
adapter: embedded:photos-style
# then
$ stunt upThe embedded: source extracts this adapter from the binary — no git clone, fully offline.
how to mock the Google Photos Library API locally
Install stunt, paste the service block above into stunt.yaml, run stunt up — then point your client at localhost instead of Google Photos Library API. It works offline, keeps state across requests and restarts, and every response is synthetic: the route shapes, status codes, pagination, and webhook delivery follow the real Google Photos Library API v1 surface — the data is fake, the behavior is real.
backing stores
tokensrefresh_tokenscodesupload_tokensmedia_itemsalbums
api surface (12)
generated from the adapter manifest — exactly what boots, nothing more
12 routes
GET/o/oauth2/auth
POST/o/oauth2/token
POST/v1/uploads
POST/v1/mediaItems:batchCreate
POST/v1/mediaItems:search
GET/v1/mediaItems
GET/v1/mediaItems/{id}
GET/v1/media-dl/{id}
GET/v1/albums
POST/v1/albums
GET/v1/albums/{id}
DELETE/v1/albums/{id}
VM-verified behaviors (50)
- ✓authorize without redirect_uri, state or client_id is 400 invalid_request
- ✓authorize redirects with a fresh code and echoes the state
- ✓a redirect_uri that already carries a query joins with &
- ✓exchanging an unknown code is 400 invalid_grant
- ✓a client mismatch answers invalid_client but leaves the code usable
- ✓the happy exchange mints the bearer pair
- ✓the minted bearer reads the data plane
- ✓authorization codes are single-use
- ✓unknown grant types are unsupported_grant_type
- ✓refresh without client credentials is 400 invalid_client
- ✓refresh with an unknown refresh token is 400 invalid_grant
- ✓refresh mints a new access token but keeps the refresh token
- ✓the refreshed token serves the same user's library
- ✓refresh-issued access tokens expire like any other
- ✓every data surface answers 401 UNAUTHENTICATED without a bearer
- ✓non-Bearer schemes and unknown or empty tokens are equally 401
- ✓the minted bearer passes the gate on every surface
- ✓the access token dies at its expiry on the virtual clock
- ✓/v1/uploads answers a plain-text uploadToken
- ✓batchCreate links the token to a media item
- ✓mime types follow the file extension
- ✓an unknown upload token answers the per-item status error
- ✓mixed batches keep request order
- ✓upload tokens are single-use
- ✓an albumId is accepted without validation (as-is)
- ✓an empty batch answers an empty result list
- ✓=d serves the original bytes with the recorded content type
- ✓=dv serves the same original bytes
- ✓a bare baseUrl serves the derivative payload instead
- ✓baseUrl is computed from the request host at read time
- ✓unknown ids answer the 404 envelope
- ✓pages walk by pageToken and the last partial page has no next
- ✓the default page holds 25 items
- ✓pageSize clamps at 100
- ✓a single item fetch and the 404 envelope
- ✓items are private to their user
- ✓an unfiltered search returns everything in creation order
- ✓mediaTypeFilter selects PHOTO or VIDEO by mime prefix
- ✓dateFilter ranges and exact dates bound creationTime
- ✓partial dates span their whole period
- ✓multiple ranges are OR'd and keep upload order
- ✓media type and date filters compose
- ✓albumId scopes the search
- ✓search paginates from the JSON body
- ✓create returns the album shape
- ✓a missing album title defaults to Untitled Album
- ✓unknown albums answer the 404 envelope
- ✓list walks pages with the 20 default and the 50 clamp
- ✓albums are private to their user
- ✓delete removes the album but leaves its media
Not implemented (5)
- No album item management (addItems, removeItems, batchRemoveMediaItems)
- No album title update (PATCH /v1/albums/{id})
- No album enrichments (text, map) via addEnrichment
- No shared albums (list, join, leave, unshare)
- No favorites marking or favorite filtering in search
Differs from the real API (5)
- Bare-id media download returns derivative bytes; =d/=dv return original upload
- search honors mediaTypeFilter and dateFilter only (no contentFilter or album filters)
- Naming both PHOTO and VIDEO in mediaTypeFilter silently keeps only the last; real API 400s
- albumId in batchCreate is not validated; mediaItemsCount stays 0 and description is dropped
- Deleting an album leaves its items findable via search {albumId}
want more of the Google Photos Library API surface? adapters are YAML + Starlark — the authoring guide covers adding routes, fixtures, and state machines. Open an issue for anything big first, or just file a PR — merged routes ship in the next release.