| 1 | package suite |
| 2 | |
| 3 | import ( |
| 4 | "embed" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | ) |
| 8 | |
| 9 | //go:embed fixtures/*.json |
| 10 | var fixtureFS embed.FS |
| 11 | |
| 12 | // Route shapes the server half mounts beside an app's own router. They are |
| 13 | // part of the contract: the conformance harness fires exactly these, so an app |
| 14 | // that mounts them elsewhere fails gate 1 rather than failing in production. |
| 15 | const ( |
| 16 | RouteS1 = "/suite/s1" // GET, ?cursor= |
| 17 | RouteS2 = "/suite/s2" // POST, body is S2Request |
| 18 | RouteS3 = "/suite/s3" // GET, /suite/s3/{item_id}?revision= |
| 19 | ) |
| 20 | |
| 21 | // S2Request is the wire shape of an authorization question: a typed asker |
| 22 | // (§3) and up to MaxS2Pairs items. The consumer token rides the transport, |
| 23 | // not the body. |
| 24 | type S2Request struct { |
| 25 | Asker Subject `json:"asker"` |
| 26 | Pairs []ItemRevision `json:"pairs"` |
| 27 | } |
| 28 | |
| 29 | // Rung is where an item sits on the sharing ladder (§4.2, ruling 15). A link |
| 30 | // is not a rung; it is a read-only capability that any rung may carry, and |
| 31 | // custody keys on the rung alone. |
| 32 | type Rung string |
| 33 | |
| 34 | const ( |
| 35 | RungPrivate Rung = "private" |
| 36 | RungGranted Rung = "granted" |
| 37 | RungTeam Rung = "team" |
| 38 | RungDeleted Rung = "deleted" |
| 39 | ) |
| 40 | |
| 41 | // CustodyCase is one row of the custody fixture: an item, where it sits, and |
| 42 | // what both calls must answer for the fixture's asker. |
| 43 | type CustodyCase struct { |
| 44 | Name string `json:"name"` |
| 45 | ItemID string `json:"item_id"` |
| 46 | Revision int64 `json:"revision"` |
| 47 | Rung Rung `json:"rung"` |
| 48 | Link string `json:"link"` |
| 49 | S3Status int `json:"s3_status"` |
| 50 | // S3Body is the exact body a content fetch must return: the item's bytes |
| 51 | // for a 200, and empty for every refusal. Checking the status alone let a |
| 52 | // server return the required 404 while including the item's secrets in the |
| 53 | // body, or return a 200 carrying a different item entirely. |
| 54 | S3Body string `json:"s3_body"` |
| 55 | S2Result Authorization `json:"s2_result"` |
| 56 | // S2ResultSuperseded is the answer when the same item is asked about one |
| 57 | // revision behind. A team item is STALE — readable, but the app of record |
| 58 | // has moved on — and anything custody already refuses stays refused. |
| 59 | S2ResultSuperseded Authorization `json:"s2_result_superseded"` |
| 60 | // S2ResultStranger is the answer for a person with no team standing. |
| 61 | S2ResultStranger Authorization `json:"s2_result_stranger"` |
| 62 | } |
| 63 | |
| 64 | // NonexistentItemID is never in the fixture. A refusal for an item that does |
| 65 | // not exist has to be byte-for-byte the refusal for one that exists and is not |
| 66 | // team-visible — that indistinguishability is the whole of §4.2, and no case |
| 67 | // in the fixture can test it, because every case is an item that exists. |
| 68 | const NonexistentItemID = "doc-no-such-item-ever" |
| 69 | |
| 70 | // CustodyFixture is gate 3's world: one case per rung, plus team-plus-link and |
| 71 | // the after-ingest transitions the reviews asked for. |
| 72 | type CustodyFixture struct { |
| 73 | Team string `json:"team"` |
| 74 | Asker Subject `json:"asker"` |
| 75 | // Stranger has no team standing. Every case refuses them, the team-visible |
| 76 | // ones included: team standing is what makes a team item readable. |
| 77 | Stranger Subject `json:"stranger"` |
| 78 | Cases []CustodyCase `json:"cases"` |
| 79 | } |
| 80 | |
| 81 | // LoadCustodyFixture returns the golden custody cases every app runs. |
| 82 | func LoadCustodyFixture() (CustodyFixture, error) { |
| 83 | raw, err := fixtureFS.ReadFile("fixtures/custody.json") |
| 84 | if err != nil { |
| 85 | return CustodyFixture{}, fmt.Errorf("suite: reading custody fixture: %w", err) |
| 86 | } |
| 87 | var f CustodyFixture |
| 88 | if err := json.Unmarshal(raw, &f); err != nil { |
| 89 | return CustodyFixture{}, fmt.Errorf("suite: parsing custody fixture: %w", err) |
| 90 | } |
| 91 | if len(f.Cases) == 0 { |
| 92 | return CustodyFixture{}, fmt.Errorf("suite: custody fixture has no cases") |
| 93 | } |
| 94 | return f, nil |
| 95 | } |
| 96 | |