mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
357 lines
12 KiB
Go
357 lines
12 KiB
Go
package did
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func testServices(publicURL string) map[string]Service {
|
|
return map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: publicURL},
|
|
"atcr_hold": {Type: "AtcrHoldService", Endpoint: publicURL},
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument verifies the standard atproto DID document layout for a did:web service.
|
|
func TestBuildDIDDocument(t *testing.T) {
|
|
publicURL := "https://hold.example.com"
|
|
signingKey := generateK256(t)
|
|
|
|
doc, err := BuildDIDDocument("did:web:hold.example.com", publicURL, signingKey, "atproto", testServices(publicURL))
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
|
|
if doc.ID != "did:web:hold.example.com" {
|
|
t.Errorf("ID: got %s want did:web:hold.example.com", doc.ID)
|
|
}
|
|
|
|
expectedContexts := []string{
|
|
"https://www.w3.org/ns/did/v1",
|
|
"https://w3id.org/security/multikey/v1",
|
|
"https://w3id.org/security/suites/secp256k1-2019/v1",
|
|
}
|
|
if len(doc.Context) != len(expectedContexts) {
|
|
t.Errorf("Context length: got %d want %d", len(doc.Context), len(expectedContexts))
|
|
}
|
|
for i, expected := range expectedContexts {
|
|
if doc.Context[i] != expected {
|
|
t.Errorf("Context[%d]: got %s want %s", i, doc.Context[i], expected)
|
|
}
|
|
}
|
|
|
|
if len(doc.AlsoKnownAs) != 1 || doc.AlsoKnownAs[0] != "at://hold.example.com" {
|
|
t.Errorf("AlsoKnownAs: got %v want [at://hold.example.com]", doc.AlsoKnownAs)
|
|
}
|
|
|
|
if len(doc.VerificationMethod) != 1 {
|
|
t.Fatalf("VerificationMethod length: got %d want 1", len(doc.VerificationMethod))
|
|
}
|
|
vm := doc.VerificationMethod[0]
|
|
if vm.ID != "did:web:hold.example.com#atproto" {
|
|
t.Errorf("VerificationMethod.ID: got %s", vm.ID)
|
|
}
|
|
if vm.Type != "Multikey" {
|
|
t.Errorf("VerificationMethod.Type: got %s want Multikey", vm.Type)
|
|
}
|
|
if vm.Controller != "did:web:hold.example.com" {
|
|
t.Errorf("VerificationMethod.Controller: got %s", vm.Controller)
|
|
}
|
|
if vm.PublicKeyMultibase == "" {
|
|
t.Error("VerificationMethod.PublicKeyMultibase is empty")
|
|
}
|
|
|
|
pub, _ := signingKey.PublicKey()
|
|
if vm.PublicKeyMultibase != pub.Multibase() {
|
|
t.Errorf("VerificationMethod.PublicKeyMultibase: got %s want %s", vm.PublicKeyMultibase, pub.Multibase())
|
|
}
|
|
|
|
if len(doc.Authentication) != 1 || doc.Authentication[0] != "did:web:hold.example.com#atproto" {
|
|
t.Errorf("Authentication: got %v", doc.Authentication)
|
|
}
|
|
|
|
if len(doc.Service) != 2 {
|
|
t.Fatalf("Service length: got %d want 2", len(doc.Service))
|
|
}
|
|
|
|
svcByID := map[string]DIDService{}
|
|
for _, s := range doc.Service {
|
|
svcByID[s.ID] = s
|
|
}
|
|
pdsService, ok := svcByID["#atproto_pds"]
|
|
if !ok {
|
|
t.Fatalf("missing #atproto_pds service in %v", svcByID)
|
|
}
|
|
if pdsService.Type != "AtprotoPersonalDataServer" {
|
|
t.Errorf("#atproto_pds Type: got %s", pdsService.Type)
|
|
}
|
|
if pdsService.ServiceEndpoint != publicURL {
|
|
t.Errorf("#atproto_pds Endpoint: got %s want %s", pdsService.ServiceEndpoint, publicURL)
|
|
}
|
|
holdService, ok := svcByID["#atcr_hold"]
|
|
if !ok {
|
|
t.Fatalf("missing #atcr_hold service in %v", svcByID)
|
|
}
|
|
if holdService.Type != "AtcrHoldService" {
|
|
t.Errorf("#atcr_hold Type: got %s", holdService.Type)
|
|
}
|
|
if holdService.ServiceEndpoint != publicURL {
|
|
t.Errorf("#atcr_hold Endpoint: got %s want %s", holdService.ServiceEndpoint, publicURL)
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument_WithPort confirms non-standard ports flow into AlsoKnownAs.
|
|
func TestBuildDIDDocument_WithPort(t *testing.T) {
|
|
publicURL := "https://hold.example.com:8443"
|
|
signingKey := generateK256(t)
|
|
|
|
doc, err := BuildDIDDocument("did:web:hold.example.com%3A8443", publicURL, signingKey, "atproto", testServices(publicURL))
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
|
|
if doc.ID != "did:web:hold.example.com%3A8443" {
|
|
t.Errorf("ID: got %s", doc.ID)
|
|
}
|
|
if doc.AlsoKnownAs[0] != "at://hold.example.com:8443" {
|
|
t.Errorf("AlsoKnownAs: got %s want at://hold.example.com:8443", doc.AlsoKnownAs[0])
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument_StandardPortsStripped verifies port 80/443 are not appended to alsoKnownAs.
|
|
func TestBuildDIDDocument_StandardPortsStripped(t *testing.T) {
|
|
signingKey := generateK256(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
publicURL string
|
|
wantAKA string
|
|
}{
|
|
{"http port 80", "http://hold.example.com:80", "at://hold.example.com"},
|
|
{"https port 443", "https://hold.example.com:443", "at://hold.example.com"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
doc, err := BuildDIDDocument("did:web:hold.example.com", tc.publicURL, signingKey, "atproto", nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
if doc.AlsoKnownAs[0] != tc.wantAKA {
|
|
t.Errorf("AlsoKnownAs: got %s want %s", doc.AlsoKnownAs[0], tc.wantAKA)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument_InvalidURL confirms malformed URLs surface as errors.
|
|
func TestBuildDIDDocument_InvalidURL(t *testing.T) {
|
|
signingKey := generateK256(t)
|
|
_, err := BuildDIDDocument("did:web:bogus", "ht!tp://invalid url", signingKey, "atproto", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid URL, got nil")
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument_DefaultVerificationKeyName confirms the empty fragment defaults to "atproto"
|
|
// and adds Authentication.
|
|
func TestBuildDIDDocument_DefaultVerificationKeyName(t *testing.T) {
|
|
signingKey := generateK256(t)
|
|
doc, err := BuildDIDDocument("did:web:example.com", "https://example.com", signingKey, "", nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
if doc.VerificationMethod[0].ID != "did:web:example.com#atproto" {
|
|
t.Errorf("VerificationMethod.ID: got %s want did:web:example.com#atproto", doc.VerificationMethod[0].ID)
|
|
}
|
|
if len(doc.Authentication) != 1 || doc.Authentication[0] != "did:web:example.com#atproto" {
|
|
t.Errorf("Authentication: got %v", doc.Authentication)
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument_LabelerKey confirms a non-"atproto" verification key (e.g. labeler)
|
|
// does not add Authentication, mirroring the bsky labeler pattern.
|
|
func TestBuildDIDDocument_LabelerKey(t *testing.T) {
|
|
signingKey := generateK256(t)
|
|
services := map[string]Service{
|
|
"atproto_labeler": {Type: "AtprotoLabeler", Endpoint: "https://labeler.example.com"},
|
|
}
|
|
doc, err := BuildDIDDocument("did:web:labeler.example.com", "https://labeler.example.com", signingKey, "atproto_label", services)
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
if doc.VerificationMethod[0].ID != "did:web:labeler.example.com#atproto_label" {
|
|
t.Errorf("VerificationMethod.ID: got %s", doc.VerificationMethod[0].ID)
|
|
}
|
|
if len(doc.Authentication) != 0 {
|
|
t.Errorf("Authentication should be empty for non-atproto key, got %v", doc.Authentication)
|
|
}
|
|
if len(doc.Service) != 1 || doc.Service[0].ID != "#atproto_labeler" {
|
|
t.Errorf("Service: got %v", doc.Service)
|
|
}
|
|
}
|
|
|
|
// TestBuildDIDDocument_NoServices confirms a DID document can be built without any service entries.
|
|
func TestBuildDIDDocument_NoServices(t *testing.T) {
|
|
signingKey := generateK256(t)
|
|
doc, err := BuildDIDDocument("did:web:example.com", "https://example.com", signingKey, "atproto", nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
if len(doc.Service) != 0 {
|
|
t.Errorf("Service should be empty, got %v", doc.Service)
|
|
}
|
|
}
|
|
|
|
// TestMarshalDIDDocument confirms marshaling produces parseable, indented JSON.
|
|
func TestMarshalDIDDocument(t *testing.T) {
|
|
signingKey := generateK256(t)
|
|
doc, err := BuildDIDDocument("did:web:example.com", "https://example.com", signingKey, "atproto", testServices("https://example.com"))
|
|
if err != nil {
|
|
t.Fatalf("BuildDIDDocument: %v", err)
|
|
}
|
|
|
|
data, err := MarshalDIDDocument(doc)
|
|
if err != nil {
|
|
t.Fatalf("MarshalDIDDocument: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(string(data), " ") {
|
|
t.Error("expected indented JSON output")
|
|
}
|
|
|
|
var parsed DIDDocument
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("Unmarshal: %v", err)
|
|
}
|
|
if parsed.ID != doc.ID {
|
|
t.Errorf("ID round-trip: got %s want %s", parsed.ID, doc.ID)
|
|
}
|
|
if len(parsed.Service) != len(doc.Service) {
|
|
t.Errorf("Service length round-trip: got %d want %d", len(parsed.Service), len(doc.Service))
|
|
}
|
|
}
|
|
|
|
// TestLoadOrCreate_DIDWeb confirms did:web mode returns a deterministic identifier
|
|
// without touching disk or any external service.
|
|
func TestLoadOrCreate_DIDWeb(t *testing.T) {
|
|
cfg := Config{
|
|
Method: "web",
|
|
PublicURL: "https://hold.example.com",
|
|
}
|
|
d, err := LoadOrCreate(context.Background(), cfg)
|
|
if err != nil {
|
|
t.Fatalf("LoadOrCreate: %v", err)
|
|
}
|
|
if d != "did:web:hold.example.com" {
|
|
t.Errorf("DID: got %s want did:web:hold.example.com", d)
|
|
}
|
|
}
|
|
|
|
// TestLoadOrCreate_DIDWebDefaultsToWebWhenMethodEmpty confirms an empty method behaves like did:web.
|
|
func TestLoadOrCreate_DIDWebDefaultsToWebWhenMethodEmpty(t *testing.T) {
|
|
cfg := Config{
|
|
PublicURL: "https://example.com:8443",
|
|
}
|
|
d, err := LoadOrCreate(context.Background(), cfg)
|
|
if err != nil {
|
|
t.Fatalf("LoadOrCreate: %v", err)
|
|
}
|
|
if d != "did:web:example.com%3A8443" {
|
|
t.Errorf("DID: got %s want did:web:example.com%%3A8443", d)
|
|
}
|
|
}
|
|
|
|
// TestLoadOrCreate_PLCRequiresVerificationKeyName confirms missing required PLC fields error early.
|
|
func TestLoadOrCreate_PLCRequiresVerificationKeyName(t *testing.T) {
|
|
cfg := Config{
|
|
Method: "plc",
|
|
PublicURL: "https://example.com",
|
|
Services: map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.com"},
|
|
},
|
|
}
|
|
_, err := LoadOrCreate(context.Background(), cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error when VerificationKeyName is empty")
|
|
}
|
|
if !strings.Contains(err.Error(), "VerificationKeyName") {
|
|
t.Errorf("expected error about VerificationKeyName, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadOrCreate_PLCRequiresServices confirms PLC mode demands at least one service entry.
|
|
func TestLoadOrCreate_PLCRequiresServices(t *testing.T) {
|
|
cfg := Config{
|
|
Method: "plc",
|
|
PublicURL: "https://example.com",
|
|
VerificationKeyName: "atproto",
|
|
}
|
|
_, err := LoadOrCreate(context.Background(), cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error when Services is empty")
|
|
}
|
|
if !strings.Contains(err.Error(), "service") {
|
|
t.Errorf("expected error about services, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadOrCreate_PLCRejectsNonPLCAdoption confirms a configured DID must be a did:plc identifier.
|
|
func TestLoadOrCreate_PLCRejectsNonPLCAdoption(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
cfg := Config{
|
|
Method: "plc",
|
|
PublicURL: "https://example.com",
|
|
DBPath: tmp,
|
|
SigningKeyPath: filepath.Join(tmp, "signing.key"),
|
|
VerificationKeyName: "atproto",
|
|
DID: "did:web:example.com",
|
|
Services: map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.com"},
|
|
},
|
|
}
|
|
_, err := LoadOrCreate(context.Background(), cfg)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-did:plc adoption")
|
|
}
|
|
if !strings.Contains(err.Error(), "did:plc") {
|
|
t.Errorf("expected error about did:plc, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLoadOrCreate_PLCAdoptionPersistsDID confirms a configured did:plc is written to did.txt
|
|
// even when the PLC directory call fails (the failure is logged, not returned).
|
|
func TestLoadOrCreate_PLCAdoptionPersistsDID(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
cfg := Config{
|
|
Method: "plc",
|
|
PublicURL: "https://example.com",
|
|
DBPath: tmp,
|
|
SigningKeyPath: filepath.Join(tmp, "signing.key"),
|
|
VerificationKeyName: "atproto",
|
|
DID: "did:plc:abcdefghijklmnopqrstuvwx",
|
|
PLCDirectoryURL: "http://127.0.0.1:1", // unreachable; EnsureCurrent failure is non-fatal
|
|
Services: map[string]Service{
|
|
"atproto_pds": {Type: "AtprotoPersonalDataServer", Endpoint: "https://example.com"},
|
|
},
|
|
}
|
|
d, err := LoadOrCreate(context.Background(), cfg)
|
|
if err != nil {
|
|
t.Fatalf("LoadOrCreate: %v", err)
|
|
}
|
|
if d != "did:plc:abcdefghijklmnopqrstuvwx" {
|
|
t.Errorf("DID: got %s", d)
|
|
}
|
|
|
|
got, err := os.ReadFile(filepath.Join(tmp, "did.txt"))
|
|
if err != nil {
|
|
t.Fatalf("read did.txt: %v", err)
|
|
}
|
|
if strings.TrimSpace(string(got)) != "did:plc:abcdefghijklmnopqrstuvwx" {
|
|
t.Errorf("did.txt contents: got %q", string(got))
|
|
}
|
|
}
|