1package testing
2
3import (
4 "bytes"
5 "context"
6 "encoding/hex"
7 "encoding/json"
8 "fmt"
9 "os"
10 "strings"
11 "testing"
12
13 appbsky "github.com/bluesky-social/indigo/api/bsky"
14 "github.com/bluesky-social/indigo/repo"
15 "github.com/bluesky-social/indigo/util"
16
17 "github.com/ipfs/go-cid"
18 "github.com/ipfs/go-datastore"
19 blockstore "github.com/ipfs/go-ipfs-blockstore"
20 "github.com/stretchr/testify/assert"
21 cbg "github.com/whyrusleeping/cbor-gen"
22 "github.com/whyrusleeping/go-did"
23)
24
25// deep verificatoin of repo: signature (against DID doc), MST structure,
26// record encoding (JSON and CBOR), etc
27func deepReproduceRepo(t *testing.T, carPath, docPath string) {
28 ctx := context.TODO()
29 assert := assert.New(t)
30
31 // NOTE bgs/bgs.go:537 if we need to parse handle from did doc
32 didDoc := mustReadDidDoc(t, docPath)
33 pubkey, err := didDoc.GetPublicKey("#atproto")
34 if err != nil {
35 t.Fatal(err)
36 }
37
38 fi, err := os.Open(carPath)
39 if err != nil {
40 t.Fatal(err)
41 }
42
43 origRepo, err := repo.ReadRepoFromCar(ctx, fi)
44 if err != nil {
45 t.Fatal(err)
46 }
47
48 // verify signature against pubkey
49 scommit := origRepo.SignedCommit()
50 msg, err := scommit.Unsigned().BytesForSigning()
51 if err != nil {
52 t.Fatal(err)
53 }
54 if err := pubkey.Verify(msg, scommit.Sig); err != nil {
55 fmt.Printf("didDoc: %v\n", didDoc)
56 fmt.Printf("key: %v\n", pubkey)
57 fmt.Printf("sig: %v\n", scommit.Sig)
58 assert.NoError(err)
59 }
60
61 // enumerate all keys
62 repoMap := make(map[string]cid.Cid)
63 err = origRepo.ForEach(ctx, "", func(k string, v cid.Cid) error {
64 repoMap[k] = v
65 return nil
66 })
67 if err != nil {
68 t.Fatal(err)
69 }
70
71 bs := blockstore.NewBlockstore(datastore.NewMapDatastore())
72 secondRepo := repo.NewRepo(ctx, didDoc.ID.String(), bs)
73 for p, c := range repoMap {
74 _, rec, err := origRepo.GetRecord(ctx, p)
75 if err != nil {
76 t.Fatal(err)
77 }
78 reproduceRecord(t, p, c, rec)
79 secondRepo.PutRecord(ctx, p, rec)
80 }
81
82 // verify MST tree reproduced
83 kmgr := &util.FakeKeyManager{}
84 _, _, err = secondRepo.Commit(ctx, kmgr.SignForUser)
85 if err != nil {
86 t.Fatal(err)
87 }
88 secondCommit := secondRepo.SignedCommit()
89 assert.Equal(scommit.Data.String(), secondCommit.Data.String())
90}
91
92// from JSON file on disk
93func mustReadDidDoc(t *testing.T, docPath string) did.Document {
94 var didDoc did.Document
95 docFile, err := os.Open(docPath)
96 if err != nil {
97 t.Fatal(err)
98 }
99 if err := json.NewDecoder(docFile).Decode(&didDoc); err != nil {
100 t.Fatal(err)
101 }
102 return didDoc
103}
104
105// deserializes and re-serializes in a couple different ways and verifies CID
106func reproduceRecord(t *testing.T, path string, c cid.Cid, rec cbg.CBORMarshaler) {
107 assert := assert.New(t)
108 // 0x71 = dag-cbor, 0x12 = sha2-256, 0 = default length
109 cidBuilder := cid.V1Builder{Codec: 0x71, MhType: 0x12, MhLength: 0}
110 recordCBOR := new(bytes.Buffer)
111 nsid := strings.SplitN(path, "/", 2)[0]
112
113 // TODO: refactor this to be short+generic
114 switch nsid {
115 case "app.bsky.feed.post":
116 var recordRepro appbsky.FeedPost
117 recordOrig, suc := rec.(*appbsky.FeedPost)
118 assert.Equal(true, suc)
119 recordJSON, err := json.Marshal(recordOrig)
120 assert.NoError(err)
121 assert.NoError(json.Unmarshal(recordJSON, &recordRepro))
122 assert.Equal(*recordOrig, recordRepro)
123 assert.NoError(recordRepro.MarshalCBOR(recordCBOR))
124 reproCID, err := cidBuilder.Sum(recordCBOR.Bytes())
125 assert.NoError(err)
126 if c.String() != reproCID.String() {
127 fmt.Println(string(recordJSON))
128 fmt.Println(hex.EncodeToString(recordCBOR.Bytes()))
129 recordBytes := new(bytes.Buffer)
130 assert.NoError(rec.MarshalCBOR(recordBytes))
131 fmt.Println(hex.EncodeToString(recordBytes.Bytes()))
132 }
133 assert.Equal(c.String(), reproCID.String())
134 case "app.bsky.actor.profile":
135 var recordRepro appbsky.ActorProfile
136 recordOrig, suc := rec.(*appbsky.ActorProfile)
137
138 assert.Equal(true, suc)
139 recordJSON, err := json.Marshal(recordOrig)
140 assert.NoError(err)
141 assert.NoError(json.Unmarshal(recordJSON, &recordRepro))
142 assert.Equal(*recordOrig, recordRepro)
143 assert.NoError(recordRepro.MarshalCBOR(recordCBOR))
144 reproCID, err := cidBuilder.Sum(recordCBOR.Bytes())
145 assert.NoError(err)
146 assert.Equal(c.String(), reproCID.String())
147 case "app.bsky.graph.follow":
148 var recordRepro appbsky.GraphFollow
149 recordOrig, suc := rec.(*appbsky.GraphFollow)
150
151 assert.Equal(true, suc)
152 recordJSON, err := json.Marshal(recordOrig)
153 assert.NoError(err)
154 assert.NoError(json.Unmarshal(recordJSON, &recordRepro))
155 assert.Equal(*recordOrig, recordRepro)
156 assert.NoError(recordRepro.MarshalCBOR(recordCBOR))
157 reproCID, err := cidBuilder.Sum(recordCBOR.Bytes())
158 assert.NoError(err)
159 assert.Equal(c.String(), reproCID.String())
160 case "app.bsky.feed.repost":
161 var recordRepro appbsky.FeedRepost
162 recordOrig, suc := rec.(*appbsky.FeedRepost)
163
164 assert.Equal(true, suc)
165 recordJSON, err := json.Marshal(recordOrig)
166 assert.NoError(err)
167 assert.NoError(json.Unmarshal(recordJSON, &recordRepro))
168 assert.Equal(*recordOrig, recordRepro)
169 assert.NoError(recordRepro.MarshalCBOR(recordCBOR))
170 reproCID, err := cidBuilder.Sum(recordCBOR.Bytes())
171 assert.NoError(err)
172 assert.Equal(c.String(), reproCID.String())
173 case "app.bsky.feed.like":
174 var recordRepro appbsky.FeedLike
175 recordOrig, suc := rec.(*appbsky.FeedLike)
176
177 assert.Equal(true, suc)
178 recordJSON, err := json.Marshal(recordOrig)
179 assert.NoError(err)
180 assert.NoError(json.Unmarshal(recordJSON, &recordRepro))
181 assert.Equal(*recordOrig, recordRepro)
182 assert.NoError(recordRepro.MarshalCBOR(recordCBOR))
183 reproCID, err := cidBuilder.Sum(recordCBOR.Bytes())
184 assert.NoError(err)
185 assert.Equal(c.String(), reproCID.String())
186 default:
187 t.Fatal(fmt.Errorf("unsupported record type: %s", nsid))
188 }
189}
190
191func TestReproduceRepo(t *testing.T) {
192
193 // to get from prod, first resolve handle then save DID doc and repo CAR file like:
194 // http get https://bsky.social/xrpc/com.atproto.identity.resolveHandle handle==greenground.bsky.social
195 // http get https://plc.directory/did:plc:wqgdnqlv2mwiio6pfchwtrff > greenground.didDoc.json
196 // http get https://bsky.social/xrpc/com.atproto.sync.getRepo did==did:plc:wqgdnqlv2mwiio6pfchwtrff > greenground.repo.car
197
198 // to fetch from local dev:
199 // http get localhost:2582/did:plc:dpg45vsnuir2vqqqadsn6afg > fakermaker.didDoc.json
200 // http get localhost:2583/xrpc/com.atproto.sync.getRepo did==did:plc:dpg45vsnuir2vqqqadsn6afg > fakermaker.repo.car
201
202 deepReproduceRepo(t, "testdata/greenground.repo.car", "testdata/greenground.didDoc.json")
203
204 // TODO: update this with the now working p256 code
205 //deepReproduceRepo(t, "testdata/fakermaker.repo.car", "testdata/fakermaker.didDoc.json")
206
207 // XXX: currently failing
208 //deepReproduceRepo(t, "testdata/paul_staging.repo.car", "testdata/paul_staging.didDoc.json")
209}