@hypercerts-org/sdk-core 0.4.0-beta.0 → 0.6.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +459 -79
- package/dist/index.cjs +128 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +28 -9
- package/dist/index.mjs +128 -47
- package/dist/index.mjs.map +1 -1
- package/dist/types.cjs +3 -2
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.ts +28 -9
- package/dist/types.mjs +3 -2
- package/dist/types.mjs.map +1 -1
- package/package.json +9 -5
- package/.turbo/turbo-build.log +0 -328
- package/.turbo/turbo-test.log +0 -118
- package/CHANGELOG.md +0 -22
- package/eslint.config.mjs +0 -22
- package/rollup.config.js +0 -75
- package/src/auth/OAuthClient.ts +0 -497
- package/src/core/SDK.ts +0 -410
- package/src/core/config.ts +0 -243
- package/src/core/errors.ts +0 -257
- package/src/core/interfaces.ts +0 -324
- package/src/core/types.ts +0 -281
- package/src/errors.ts +0 -57
- package/src/index.ts +0 -107
- package/src/lexicons.ts +0 -64
- package/src/repository/BlobOperationsImpl.ts +0 -199
- package/src/repository/CollaboratorOperationsImpl.ts +0 -396
- package/src/repository/HypercertOperationsImpl.ts +0 -1146
- package/src/repository/LexiconRegistry.ts +0 -332
- package/src/repository/OrganizationOperationsImpl.ts +0 -234
- package/src/repository/ProfileOperationsImpl.ts +0 -281
- package/src/repository/RecordOperationsImpl.ts +0 -340
- package/src/repository/Repository.ts +0 -482
- package/src/repository/interfaces.ts +0 -897
- package/src/repository/types.ts +0 -111
- package/src/services/hypercerts/types.ts +0 -87
- package/src/storage/InMemorySessionStore.ts +0 -127
- package/src/storage/InMemoryStateStore.ts +0 -146
- package/src/storage.ts +0 -63
- package/src/testing/index.ts +0 -67
- package/src/testing/mocks.ts +0 -142
- package/src/testing/stores.ts +0 -285
- package/src/testing.ts +0 -64
- package/src/types.ts +0 -86
- package/tests/auth/OAuthClient.test.ts +0 -164
- package/tests/core/SDK.test.ts +0 -176
- package/tests/core/errors.test.ts +0 -81
- package/tests/repository/BlobOperationsImpl.test.ts +0 -154
- package/tests/repository/CollaboratorOperationsImpl.test.ts +0 -438
- package/tests/repository/HypercertOperationsImpl.test.ts +0 -652
- package/tests/repository/LexiconRegistry.test.ts +0 -192
- package/tests/repository/OrganizationOperationsImpl.test.ts +0 -242
- package/tests/repository/ProfileOperationsImpl.test.ts +0 -254
- package/tests/repository/RecordOperationsImpl.test.ts +0 -375
- package/tests/repository/Repository.test.ts +0 -149
- package/tests/utils/fixtures.ts +0 -117
- package/tests/utils/mocks.ts +0 -109
- package/tests/utils/repository-fixtures.ts +0 -78
- package/tsconfig.json +0 -11
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -30
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { LexiconRegistry } from "../../src/repository/LexiconRegistry.js";
|
|
3
|
-
import { ValidationError } from "../../src/core/errors.js";
|
|
4
|
-
import { createMockLexicon } from "../utils/repository-fixtures.js";
|
|
5
|
-
import type { Agent } from "@atproto/api";
|
|
6
|
-
import type { LexiconDoc } from "@atproto/lexicon";
|
|
7
|
-
|
|
8
|
-
describe("LexiconRegistry", () => {
|
|
9
|
-
let registry: LexiconRegistry;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
registry = new LexiconRegistry();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("register", () => {
|
|
16
|
-
it("should register a lexicon", () => {
|
|
17
|
-
const lexicon = createMockLexicon("com.example.test");
|
|
18
|
-
expect(() => registry.register(lexicon as LexiconDoc)).not.toThrow();
|
|
19
|
-
expect(registry.has("com.example.test")).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("should throw ValidationError for lexicon without id", () => {
|
|
23
|
-
const lexicon = { lexicon: 1 } as unknown as LexiconDoc;
|
|
24
|
-
expect(() => registry.register(lexicon)).toThrow(ValidationError);
|
|
25
|
-
expect(() => registry.register(lexicon)).toThrow("Lexicon must have an 'id' field");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should overwrite existing lexicon with same id", () => {
|
|
29
|
-
const lexicon1 = createMockLexicon("com.example.test");
|
|
30
|
-
const lexicon2 = {
|
|
31
|
-
...lexicon1,
|
|
32
|
-
defs: {
|
|
33
|
-
record: {
|
|
34
|
-
type: "record" as const,
|
|
35
|
-
record: {
|
|
36
|
-
type: "object" as const,
|
|
37
|
-
properties: {},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
} as LexiconDoc;
|
|
42
|
-
|
|
43
|
-
registry.register(lexicon1);
|
|
44
|
-
// Overwriting lexicons is allowed (creates new Lexicons collection)
|
|
45
|
-
expect(() => registry.register(lexicon2)).not.toThrow();
|
|
46
|
-
expect(registry.get("com.example.test")).toEqual(lexicon2);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("registerMany", () => {
|
|
51
|
-
it("should register multiple lexicons", () => {
|
|
52
|
-
const lexicons = [
|
|
53
|
-
createMockLexicon("com.example.one"),
|
|
54
|
-
createMockLexicon("com.example.two"),
|
|
55
|
-
createMockLexicon("com.example.three"),
|
|
56
|
-
] as LexiconDoc[];
|
|
57
|
-
|
|
58
|
-
registry.registerMany(lexicons);
|
|
59
|
-
|
|
60
|
-
expect(registry.has("com.example.one")).toBe(true);
|
|
61
|
-
expect(registry.has("com.example.two")).toBe(true);
|
|
62
|
-
expect(registry.has("com.example.three")).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("should throw ValidationError if any lexicon is invalid", () => {
|
|
66
|
-
const lexicons = [createMockLexicon("com.example.one"), { lexicon: 1 } as unknown as LexiconDoc];
|
|
67
|
-
|
|
68
|
-
expect(() => registry.registerMany(lexicons)).toThrow(ValidationError);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("get", () => {
|
|
73
|
-
it("should return registered lexicon", () => {
|
|
74
|
-
const lexicon = createMockLexicon("com.example.test");
|
|
75
|
-
registry.register(lexicon);
|
|
76
|
-
|
|
77
|
-
const retrieved = registry.get("com.example.test");
|
|
78
|
-
expect(retrieved).toEqual(lexicon);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("should return undefined for unregistered lexicon", () => {
|
|
82
|
-
expect(registry.get("com.example.nonexistent")).toBeUndefined();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("validate", () => {
|
|
87
|
-
it("should validate a valid record", () => {
|
|
88
|
-
const lexicon = createMockLexicon("com.example.test");
|
|
89
|
-
registry.register(lexicon);
|
|
90
|
-
|
|
91
|
-
// The mock lexicon has a record def, so we need to validate against the record type
|
|
92
|
-
// Note: Validation may fail if the schema doesn't match exactly
|
|
93
|
-
const result = registry.validate("com.example.test", {
|
|
94
|
-
$type: "com.example.test",
|
|
95
|
-
text: "Hello",
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Validation result depends on lexicon schema - just check it doesn't throw
|
|
99
|
-
expect(result).toHaveProperty("valid");
|
|
100
|
-
// Error property is optional and only present when validation fails
|
|
101
|
-
if (!result.valid) {
|
|
102
|
-
expect(result).toHaveProperty("error");
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("should return invalid result for invalid record", () => {
|
|
107
|
-
const lexicon = createMockLexicon("com.example.test");
|
|
108
|
-
registry.register(lexicon);
|
|
109
|
-
|
|
110
|
-
const result = registry.validate("com.example.test", {
|
|
111
|
-
$type: "com.example.test",
|
|
112
|
-
invalidField: "value",
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Validation may pass or fail depending on lexicon schema
|
|
116
|
-
// The important thing is that validate() doesn't throw
|
|
117
|
-
expect(result).toHaveProperty("valid");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("should return valid result for unknown collection (no lexicon registered)", () => {
|
|
121
|
-
// When no lexicon is registered for a collection, validation passes
|
|
122
|
-
// because we can't validate against unknown schemas
|
|
123
|
-
const result = registry.validate("com.example.unknown", {
|
|
124
|
-
$type: "com.example.unknown",
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
expect(result.valid).toBe(true);
|
|
128
|
-
// No error when lexicon isn't registered
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("addToAgent", () => {
|
|
133
|
-
it("should add lexicons to agent", () => {
|
|
134
|
-
const lexicon = createMockLexicon("com.example.test");
|
|
135
|
-
registry.register(lexicon);
|
|
136
|
-
|
|
137
|
-
const mockAgent = {
|
|
138
|
-
lex: {
|
|
139
|
-
add: vi.fn(),
|
|
140
|
-
},
|
|
141
|
-
} as unknown as Agent;
|
|
142
|
-
|
|
143
|
-
registry.addToAgent(mockAgent);
|
|
144
|
-
|
|
145
|
-
expect(mockAgent.lex.add).toHaveBeenCalledWith(lexicon);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("should add all registered lexicons to agent", () => {
|
|
149
|
-
const lexicons = [createMockLexicon("com.example.one"), createMockLexicon("com.example.two")];
|
|
150
|
-
registry.registerMany(lexicons);
|
|
151
|
-
|
|
152
|
-
const mockAgent = {
|
|
153
|
-
lex: {
|
|
154
|
-
add: vi.fn(),
|
|
155
|
-
},
|
|
156
|
-
} as unknown as Agent;
|
|
157
|
-
|
|
158
|
-
registry.addToAgent(mockAgent);
|
|
159
|
-
|
|
160
|
-
expect(mockAgent.lex.add).toHaveBeenCalledTimes(2);
|
|
161
|
-
expect(mockAgent.lex.add).toHaveBeenCalledWith(lexicons[0]);
|
|
162
|
-
expect(mockAgent.lex.add).toHaveBeenCalledWith(lexicons[1]);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("getRegisteredIds", () => {
|
|
167
|
-
it("should return empty array for empty registry", () => {
|
|
168
|
-
expect(registry.getRegisteredIds()).toEqual([]);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("should return all registered lexicon IDs", () => {
|
|
172
|
-
registry.register(createMockLexicon("com.example.one"));
|
|
173
|
-
registry.register(createMockLexicon("com.example.two"));
|
|
174
|
-
|
|
175
|
-
const ids = registry.getRegisteredIds();
|
|
176
|
-
expect(ids).toContain("com.example.one");
|
|
177
|
-
expect(ids).toContain("com.example.two");
|
|
178
|
-
expect(ids.length).toBe(2);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("has", () => {
|
|
183
|
-
it("should return true for registered lexicon", () => {
|
|
184
|
-
registry.register(createMockLexicon("com.example.test"));
|
|
185
|
-
expect(registry.has("com.example.test")).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("should return false for unregistered lexicon", () => {
|
|
189
|
-
expect(registry.has("com.example.nonexistent")).toBe(false);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { OrganizationOperationsImpl } from "../../src/repository/OrganizationOperationsImpl.js";
|
|
3
|
-
import { NetworkError } from "../../src/core/errors.js";
|
|
4
|
-
|
|
5
|
-
describe("OrganizationOperationsImpl", () => {
|
|
6
|
-
let mockSession: any;
|
|
7
|
-
let orgOps: OrganizationOperationsImpl;
|
|
8
|
-
const repoDid = "did:plc:testdid123";
|
|
9
|
-
const serverUrl = "https://sds.example.com";
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
mockSession = {
|
|
13
|
-
did: "did:plc:user123",
|
|
14
|
-
sub: "did:plc:user123",
|
|
15
|
-
fetchHandler: vi.fn(),
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
orgOps = new OrganizationOperationsImpl(mockSession, repoDid, serverUrl);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe("create", () => {
|
|
22
|
-
it("should create an organization successfully", async () => {
|
|
23
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
24
|
-
ok: true,
|
|
25
|
-
json: async () => ({
|
|
26
|
-
did: "did:plc:neworg123",
|
|
27
|
-
handle: "myorg.example.com",
|
|
28
|
-
name: "My Organization",
|
|
29
|
-
description: "A test organization",
|
|
30
|
-
createdAt: "2024-01-01T00:00:00Z",
|
|
31
|
-
}),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const result = await orgOps.create({
|
|
35
|
-
name: "My Organization",
|
|
36
|
-
description: "A test organization",
|
|
37
|
-
handle: "myorg",
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(result.did).toBe("did:plc:neworg123");
|
|
41
|
-
expect(result.handle).toBe("myorg.example.com");
|
|
42
|
-
expect(result.name).toBe("My Organization");
|
|
43
|
-
expect(result.accessType).toBe("owner");
|
|
44
|
-
expect(result.permissions.owner).toBe(true);
|
|
45
|
-
|
|
46
|
-
expect(mockSession.fetchHandler).toHaveBeenCalledWith(
|
|
47
|
-
`${serverUrl}/xrpc/com.sds.organization.create`,
|
|
48
|
-
expect.objectContaining({
|
|
49
|
-
method: "POST",
|
|
50
|
-
headers: { "Content-Type": "application/json" },
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("should create organization with minimal params", async () => {
|
|
56
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
57
|
-
ok: true,
|
|
58
|
-
json: async () => ({
|
|
59
|
-
did: "did:plc:org",
|
|
60
|
-
handle: "org.example.com",
|
|
61
|
-
name: "Org",
|
|
62
|
-
}),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const result = await orgOps.create({ name: "Org" });
|
|
66
|
-
|
|
67
|
-
expect(result.name).toBe("Org");
|
|
68
|
-
expect(result.createdAt).toBeDefined(); // Should default to current time
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("should throw NetworkError on failure", async () => {
|
|
72
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
73
|
-
ok: false,
|
|
74
|
-
statusText: "Conflict",
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
await expect(
|
|
78
|
-
orgOps.create({ name: "Test Org" }),
|
|
79
|
-
).rejects.toThrow(NetworkError);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("get", () => {
|
|
84
|
-
it("should get an organization by DID", async () => {
|
|
85
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
86
|
-
ok: true,
|
|
87
|
-
json: async () => ({
|
|
88
|
-
repositories: [
|
|
89
|
-
{
|
|
90
|
-
did: "did:plc:org1",
|
|
91
|
-
handle: "org1.example.com",
|
|
92
|
-
name: "Organization 1",
|
|
93
|
-
description: "First org",
|
|
94
|
-
accessType: "owner",
|
|
95
|
-
permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
did: "did:plc:org2",
|
|
99
|
-
handle: "org2.example.com",
|
|
100
|
-
name: "Organization 2",
|
|
101
|
-
accessType: "collaborator",
|
|
102
|
-
permissions: { read: true, create: true, update: true, delete: false, admin: false, owner: false },
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
}),
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const result = await orgOps.get("did:plc:org1");
|
|
109
|
-
|
|
110
|
-
expect(result).not.toBeNull();
|
|
111
|
-
expect(result!.did).toBe("did:plc:org1");
|
|
112
|
-
expect(result!.name).toBe("Organization 1");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("should return null for non-existent organization", async () => {
|
|
116
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
117
|
-
ok: true,
|
|
118
|
-
json: async () => ({
|
|
119
|
-
repositories: [],
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const result = await orgOps.get("did:plc:nonexistent");
|
|
124
|
-
|
|
125
|
-
expect(result).toBeNull();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("should return null on error", async () => {
|
|
129
|
-
mockSession.fetchHandler.mockRejectedValue(new Error("Network error"));
|
|
130
|
-
|
|
131
|
-
const result = await orgOps.get("did:plc:org");
|
|
132
|
-
|
|
133
|
-
expect(result).toBeNull();
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe("list", () => {
|
|
138
|
-
it("should list all accessible organizations", async () => {
|
|
139
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
140
|
-
ok: true,
|
|
141
|
-
json: async () => ({
|
|
142
|
-
repositories: [
|
|
143
|
-
{
|
|
144
|
-
did: "did:plc:org1",
|
|
145
|
-
handle: "org1.example.com",
|
|
146
|
-
name: "Organization 1",
|
|
147
|
-
accessType: "owner",
|
|
148
|
-
permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
did: "did:plc:org2",
|
|
152
|
-
handle: "org2.example.com",
|
|
153
|
-
name: "Organization 2",
|
|
154
|
-
description: "Second org",
|
|
155
|
-
accessType: "collaborator",
|
|
156
|
-
permissions: { read: true, create: true, update: false, delete: false, admin: false, owner: false },
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
}),
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const result = await orgOps.list();
|
|
163
|
-
|
|
164
|
-
expect(result).toHaveLength(2);
|
|
165
|
-
expect(result[0].did).toBe("did:plc:org1");
|
|
166
|
-
expect(result[0].accessType).toBe("owner");
|
|
167
|
-
expect(result[1].did).toBe("did:plc:org2");
|
|
168
|
-
expect(result[1].accessType).toBe("collaborator");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("should handle empty repositories list", async () => {
|
|
172
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
173
|
-
ok: true,
|
|
174
|
-
json: async () => ({ repositories: [] }),
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const result = await orgOps.list();
|
|
178
|
-
|
|
179
|
-
expect(result).toHaveLength(0);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("should use session DID in query", async () => {
|
|
183
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
184
|
-
ok: true,
|
|
185
|
-
json: async () => ({ repositories: [] }),
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
await orgOps.list();
|
|
189
|
-
|
|
190
|
-
expect(mockSession.fetchHandler).toHaveBeenCalledWith(
|
|
191
|
-
expect.stringContaining(`userDid=${encodeURIComponent(mockSession.did)}`),
|
|
192
|
-
expect.any(Object),
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("should use session.sub if did is not available", async () => {
|
|
197
|
-
mockSession.did = undefined;
|
|
198
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
199
|
-
ok: true,
|
|
200
|
-
json: async () => ({ repositories: [] }),
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
await orgOps.list();
|
|
204
|
-
|
|
205
|
-
expect(mockSession.fetchHandler).toHaveBeenCalledWith(
|
|
206
|
-
expect.stringContaining(`userDid=${encodeURIComponent(mockSession.sub)}`),
|
|
207
|
-
expect.any(Object),
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("should throw NetworkError on failure", async () => {
|
|
212
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
213
|
-
ok: false,
|
|
214
|
-
statusText: "Internal Server Error",
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
await expect(orgOps.list()).rejects.toThrow(NetworkError);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("should add createdAt for organizations without it", async () => {
|
|
221
|
-
mockSession.fetchHandler.mockResolvedValue({
|
|
222
|
-
ok: true,
|
|
223
|
-
json: async () => ({
|
|
224
|
-
repositories: [
|
|
225
|
-
{
|
|
226
|
-
did: "did:plc:org",
|
|
227
|
-
handle: "org.example.com",
|
|
228
|
-
name: "Org",
|
|
229
|
-
accessType: "owner",
|
|
230
|
-
permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
|
|
231
|
-
// No createdAt field
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
}),
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const result = await orgOps.list();
|
|
238
|
-
|
|
239
|
-
expect(result[0].createdAt).toBeDefined();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
});
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { ProfileOperationsImpl } from "../../src/repository/ProfileOperationsImpl.js";
|
|
3
|
-
import { NetworkError } from "../../src/core/errors.js";
|
|
4
|
-
|
|
5
|
-
describe("ProfileOperationsImpl", () => {
|
|
6
|
-
let mockAgent: any;
|
|
7
|
-
let profileOps: ProfileOperationsImpl;
|
|
8
|
-
const repoDid = "did:plc:testdid123";
|
|
9
|
-
const serverUrl = "https://pds.example.com";
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
mockAgent = {
|
|
13
|
-
getProfile: vi.fn(),
|
|
14
|
-
com: {
|
|
15
|
-
atproto: {
|
|
16
|
-
repo: {
|
|
17
|
-
getRecord: vi.fn(),
|
|
18
|
-
putRecord: vi.fn(),
|
|
19
|
-
uploadBlob: vi.fn(),
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
profileOps = new ProfileOperationsImpl(mockAgent, repoDid, serverUrl);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("get", () => {
|
|
29
|
-
it("should get profile successfully", async () => {
|
|
30
|
-
mockAgent.getProfile.mockResolvedValue({
|
|
31
|
-
success: true,
|
|
32
|
-
data: {
|
|
33
|
-
handle: "test.bsky.social",
|
|
34
|
-
displayName: "Test User",
|
|
35
|
-
description: "A test user",
|
|
36
|
-
avatar: "https://example.com/avatar.jpg",
|
|
37
|
-
banner: "https://example.com/banner.jpg",
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const result = await profileOps.get();
|
|
42
|
-
|
|
43
|
-
expect(result.handle).toBe("test.bsky.social");
|
|
44
|
-
expect(result.displayName).toBe("Test User");
|
|
45
|
-
expect(result.description).toBe("A test user");
|
|
46
|
-
expect(result.avatar).toBe("https://example.com/avatar.jpg");
|
|
47
|
-
expect(result.banner).toBe("https://example.com/banner.jpg");
|
|
48
|
-
expect(mockAgent.getProfile).toHaveBeenCalledWith({ actor: repoDid });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("should handle profile without optional fields", async () => {
|
|
52
|
-
mockAgent.getProfile.mockResolvedValue({
|
|
53
|
-
success: true,
|
|
54
|
-
data: {
|
|
55
|
-
handle: "minimal.bsky.social",
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const result = await profileOps.get();
|
|
60
|
-
|
|
61
|
-
expect(result.handle).toBe("minimal.bsky.social");
|
|
62
|
-
expect(result.displayName).toBeUndefined();
|
|
63
|
-
expect(result.description).toBeUndefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("should throw NetworkError when API returns success: false", async () => {
|
|
67
|
-
mockAgent.getProfile.mockResolvedValue({
|
|
68
|
-
success: false,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
await expect(profileOps.get()).rejects.toThrow(NetworkError);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("should throw NetworkError when API throws", async () => {
|
|
75
|
-
mockAgent.getProfile.mockRejectedValue(new Error("Profile not found"));
|
|
76
|
-
|
|
77
|
-
await expect(profileOps.get()).rejects.toThrow(NetworkError);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("update", () => {
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
// Default mock for getting existing profile
|
|
84
|
-
mockAgent.com.atproto.repo.getRecord.mockResolvedValue({
|
|
85
|
-
success: true,
|
|
86
|
-
data: {
|
|
87
|
-
value: {
|
|
88
|
-
displayName: "Old Name",
|
|
89
|
-
description: "Old description",
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
mockAgent.com.atproto.repo.putRecord.mockResolvedValue({
|
|
95
|
-
success: true,
|
|
96
|
-
data: {
|
|
97
|
-
uri: "at://did:plc:test/app.bsky.actor.profile/self",
|
|
98
|
-
cid: "bafyrei123",
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("should update displayName", async () => {
|
|
104
|
-
const result = await profileOps.update({
|
|
105
|
-
displayName: "New Name",
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(result.uri).toBe("at://did:plc:test/app.bsky.actor.profile/self");
|
|
109
|
-
expect(mockAgent.com.atproto.repo.putRecord).toHaveBeenCalledWith(
|
|
110
|
-
expect.objectContaining({
|
|
111
|
-
record: expect.objectContaining({
|
|
112
|
-
displayName: "New Name",
|
|
113
|
-
description: "Old description",
|
|
114
|
-
}),
|
|
115
|
-
}),
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("should update description", async () => {
|
|
120
|
-
await profileOps.update({
|
|
121
|
-
description: "New description",
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
expect(mockAgent.com.atproto.repo.putRecord).toHaveBeenCalledWith(
|
|
125
|
-
expect.objectContaining({
|
|
126
|
-
record: expect.objectContaining({
|
|
127
|
-
displayName: "Old Name",
|
|
128
|
-
description: "New description",
|
|
129
|
-
}),
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("should remove displayName when set to null", async () => {
|
|
135
|
-
await profileOps.update({
|
|
136
|
-
displayName: null,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const putCall = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0];
|
|
140
|
-
expect(putCall.record.displayName).toBeUndefined();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("should remove description when set to null", async () => {
|
|
144
|
-
await profileOps.update({
|
|
145
|
-
description: null,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const putCall = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0];
|
|
149
|
-
expect(putCall.record.description).toBeUndefined();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("should upload and set avatar", async () => {
|
|
153
|
-
const avatarBlob = new Blob(["avatar data"], { type: "image/jpeg" });
|
|
154
|
-
mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
|
|
155
|
-
success: true,
|
|
156
|
-
data: {
|
|
157
|
-
blob: { ref: { $link: "avatar-cid" }, mimeType: "image/jpeg", size: 100 },
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await profileOps.update({
|
|
162
|
-
avatar: avatarBlob,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalled();
|
|
166
|
-
expect(mockAgent.com.atproto.repo.putRecord).toHaveBeenCalledWith(
|
|
167
|
-
expect.objectContaining({
|
|
168
|
-
record: expect.objectContaining({
|
|
169
|
-
avatar: expect.objectContaining({ ref: { $link: "avatar-cid" } }),
|
|
170
|
-
}),
|
|
171
|
-
}),
|
|
172
|
-
);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("should remove avatar when set to null", async () => {
|
|
176
|
-
mockAgent.com.atproto.repo.getRecord.mockResolvedValue({
|
|
177
|
-
success: true,
|
|
178
|
-
data: {
|
|
179
|
-
value: {
|
|
180
|
-
displayName: "Name",
|
|
181
|
-
avatar: { ref: { $link: "old-avatar" } },
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await profileOps.update({
|
|
187
|
-
avatar: null,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const putCall = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0];
|
|
191
|
-
expect(putCall.record.avatar).toBeUndefined();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("should upload and set banner", async () => {
|
|
195
|
-
const bannerBlob = new Blob(["banner data"], { type: "image/jpeg" });
|
|
196
|
-
mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
|
|
197
|
-
success: true,
|
|
198
|
-
data: {
|
|
199
|
-
blob: { ref: { $link: "banner-cid" }, mimeType: "image/jpeg", size: 200 },
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
await profileOps.update({
|
|
204
|
-
banner: bannerBlob,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalled();
|
|
208
|
-
expect(mockAgent.com.atproto.repo.putRecord).toHaveBeenCalledWith(
|
|
209
|
-
expect.objectContaining({
|
|
210
|
-
record: expect.objectContaining({
|
|
211
|
-
banner: expect.objectContaining({ ref: { $link: "banner-cid" } }),
|
|
212
|
-
}),
|
|
213
|
-
}),
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("should remove banner when set to null", async () => {
|
|
218
|
-
mockAgent.com.atproto.repo.getRecord.mockResolvedValue({
|
|
219
|
-
success: true,
|
|
220
|
-
data: {
|
|
221
|
-
value: {
|
|
222
|
-
displayName: "Name",
|
|
223
|
-
banner: { ref: { $link: "old-banner" } },
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
await profileOps.update({
|
|
229
|
-
banner: null,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const putCall = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0];
|
|
233
|
-
expect(putCall.record.banner).toBeUndefined();
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("should throw NetworkError when putRecord returns success: false", async () => {
|
|
237
|
-
mockAgent.com.atproto.repo.putRecord.mockResolvedValue({
|
|
238
|
-
success: false,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await expect(
|
|
242
|
-
profileOps.update({ displayName: "New Name" }),
|
|
243
|
-
).rejects.toThrow(NetworkError);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("should throw NetworkError when API throws", async () => {
|
|
247
|
-
mockAgent.com.atproto.repo.putRecord.mockRejectedValue(new Error("Update failed"));
|
|
248
|
-
|
|
249
|
-
await expect(
|
|
250
|
-
profileOps.update({ displayName: "New Name" }),
|
|
251
|
-
).rejects.toThrow(NetworkError);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
});
|