package api import ( "context" "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/json" "github.com/Fuwn/plutia/internal/config" "github.com/Fuwn/plutia/internal/ingest" "github.com/Fuwn/plutia/internal/storage" "github.com/Fuwn/plutia/internal/types" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) func TestThinModeResolveCompatibilityAndProofNotImplemented(t *testing.T) { did := "did:plc:thin-http" upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/"+did+"/log" { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(thinHTTPOperationLog(t, did)) })) defer upstream.Close() tmp := t.TempDir() dataDir := filepath.Join(tmp, "data") if err := os.MkdirAll(dataDir, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } store, err := storage.OpenPebble(dataDir) if err != nil { t.Fatalf("open pebble: %v", err) } defer store.Close() if err := store.SetMode(config.ModeThin); err != nil { t.Fatalf("set mode: %v", err) } cfg := config.Default() cfg.Mode = config.ModeThin cfg.DataDir = dataDir cfg.PLCSource = upstream.URL cfg.ThinCacheTTL = 24 * time.Hour cfg.ThinCacheMaxEntries = 1000 svc := ingest.NewService(cfg, store, ingest.NewClient(upstream.URL), nil, nil) defer svc.Close() ts := httptest.NewServer(NewServer(cfg, store, svc, nil).Handler()) defer ts.Close() resp, err := http.Get(ts.URL + "/" + did) if err != nil { t.Fatalf("compat resolve: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("compat resolve status: got %d want 200", resp.StatusCode) } if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "application/did+ld+json") { t.Fatalf("compat resolve content-type mismatch: %s", got) } var doc map[string]any if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { t.Fatalf("decode did doc: %v", err) } if got, _ := doc["id"].(string); got != did { t.Fatalf("did doc id mismatch: got %q want %q", got, did) } proofResp, err := http.Get(ts.URL + "/did/" + did + "/proof") if err != nil { t.Fatalf("thin proof request: %v", err) } defer proofResp.Body.Close() if proofResp.StatusCode != http.StatusNotImplemented { t.Fatalf("thin proof status: got %d want 501", proofResp.StatusCode) } var statusBody map[string]any statusResp, err := http.Get(ts.URL + "/status") if err != nil { t.Fatalf("status request: %v", err) } defer statusResp.Body.Close() statusBytes, err := io.ReadAll(statusResp.Body) if err != nil { t.Fatalf("read status body: %v", err) } t.Logf("thin_status=%s", strings.TrimSpace(string(statusBytes))) if err := json.Unmarshal(statusBytes, &statusBody); err != nil { t.Fatalf("decode status: %v", err) } if mode, _ := statusBody["mode"].(string); mode != config.ModeThin { t.Fatalf("status mode mismatch: got %q want %q", mode, config.ModeThin) } if err := svc.VerifyDID(context.Background(), did); err != nil { t.Fatalf("verify thin did: %v", err) } } func thinHTTPOperationLog(t *testing.T, did string) []map[string]any { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatalf("generate key: %v", err) } ops := make([]map[string]any, 0, 2) prev := "" for i := 0; i < 2; i++ { unsigned := map[string]any{ "did": did, "didDoc": map[string]any{"id": did, "seq": i + 1}, "publicKey": base64.RawURLEncoding.EncodeToString(pub), } if prev != "" { unsigned["prev"] = prev } payload, _ := json.Marshal(unsigned) canon, _ := types.CanonicalizeJSON(payload) sig := ed25519.Sign(priv, canon) op := map[string]any{} for k, v := range unsigned { op[k] = v } op["sigPayload"] = base64.RawURLEncoding.EncodeToString(canon) op["sig"] = base64.RawURLEncoding.EncodeToString(sig) raw, _ := json.Marshal(op) parsed, err := types.ParseOperation(types.ExportRecord{Seq: uint64(i + 1), DID: did, Operation: raw}) if err != nil { t.Fatalf("parse operation: %v", err) } prev = parsed.CID ops = append(ops, map[string]any{ "operation": op, "cid": parsed.CID, "createdAt": time.Now().UTC().Add(time.Duration(i) * time.Second).Format(time.RFC3339), "nullified": false, }) } return ops }