@hypercerts-org/sdk-core 0.5.0-beta.0 → 0.7.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.
Files changed (57) hide show
  1. package/README.md +130 -8
  2. package/dist/index.cjs +93 -15
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +64 -1
  5. package/dist/index.mjs +93 -16
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +9 -5
  8. package/.turbo/turbo-build.log +0 -40
  9. package/.turbo/turbo-test.log +0 -119
  10. package/CHANGELOG.md +0 -62
  11. package/eslint.config.mjs +0 -22
  12. package/rollup.config.js +0 -75
  13. package/src/auth/OAuthClient.ts +0 -497
  14. package/src/core/SDK.ts +0 -410
  15. package/src/core/config.ts +0 -243
  16. package/src/core/errors.ts +0 -257
  17. package/src/core/interfaces.ts +0 -324
  18. package/src/core/types.ts +0 -282
  19. package/src/errors.ts +0 -57
  20. package/src/index.ts +0 -107
  21. package/src/lexicons.ts +0 -64
  22. package/src/repository/BlobOperationsImpl.ts +0 -199
  23. package/src/repository/CollaboratorOperationsImpl.ts +0 -442
  24. package/src/repository/HypercertOperationsImpl.ts +0 -1146
  25. package/src/repository/LexiconRegistry.ts +0 -332
  26. package/src/repository/OrganizationOperationsImpl.ts +0 -282
  27. package/src/repository/ProfileOperationsImpl.ts +0 -281
  28. package/src/repository/RecordOperationsImpl.ts +0 -340
  29. package/src/repository/Repository.ts +0 -482
  30. package/src/repository/interfaces.ts +0 -909
  31. package/src/repository/types.ts +0 -111
  32. package/src/services/hypercerts/types.ts +0 -87
  33. package/src/storage/InMemorySessionStore.ts +0 -127
  34. package/src/storage/InMemoryStateStore.ts +0 -146
  35. package/src/storage.ts +0 -63
  36. package/src/testing/index.ts +0 -67
  37. package/src/testing/mocks.ts +0 -142
  38. package/src/testing/stores.ts +0 -285
  39. package/src/testing.ts +0 -64
  40. package/src/types.ts +0 -86
  41. package/tests/auth/OAuthClient.test.ts +0 -164
  42. package/tests/core/SDK.test.ts +0 -176
  43. package/tests/core/errors.test.ts +0 -81
  44. package/tests/repository/BlobOperationsImpl.test.ts +0 -155
  45. package/tests/repository/CollaboratorOperationsImpl.test.ts +0 -438
  46. package/tests/repository/HypercertOperationsImpl.test.ts +0 -652
  47. package/tests/repository/LexiconRegistry.test.ts +0 -192
  48. package/tests/repository/OrganizationOperationsImpl.test.ts +0 -240
  49. package/tests/repository/ProfileOperationsImpl.test.ts +0 -254
  50. package/tests/repository/RecordOperationsImpl.test.ts +0 -375
  51. package/tests/repository/Repository.test.ts +0 -149
  52. package/tests/utils/fixtures.ts +0 -117
  53. package/tests/utils/mocks.ts +0 -109
  54. package/tests/utils/repository-fixtures.ts +0 -78
  55. package/tsconfig.json +0 -11
  56. package/tsconfig.tsbuildinfo +0 -1
  57. package/vitest.config.ts +0 -30
@@ -1,164 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { OAuthClient } from "../../src/auth/OAuthClient.js";
3
- import { AuthenticationError } from "../../src/core/errors.js";
4
- import { createTestConfig, createTestConfigAsync } from "../utils/fixtures.js";
5
- import { InMemorySessionStore, InMemoryStateStore } from "../utils/mocks.js";
6
-
7
- describe("OAuthClient", () => {
8
- let config: ReturnType<typeof createTestConfig>;
9
-
10
- beforeEach(() => {
11
- config = createTestConfig();
12
- });
13
-
14
- describe("constructor", () => {
15
- it("should initialize with valid config", () => {
16
- expect(() => new OAuthClient(config)).not.toThrow();
17
- });
18
-
19
- it("should throw AuthenticationError for invalid JWK", async () => {
20
- const invalidConfig = await createTestConfigAsync({
21
- oauth: {
22
- ...config.oauth,
23
- jwkPrivate: "invalid json",
24
- },
25
- });
26
-
27
- expect(() => new OAuthClient(invalidConfig)).toThrow(AuthenticationError);
28
- expect(() => new OAuthClient(invalidConfig)).toThrow("Failed to parse JWK private key");
29
- });
30
-
31
- it("should use custom fetch handler if provided", async () => {
32
- const customFetch = vi.fn();
33
- const configWithFetch = await createTestConfigAsync({
34
- fetch: customFetch,
35
- });
36
- const client = new OAuthClient(configWithFetch);
37
- expect(client).toBeDefined();
38
- });
39
-
40
- it("should use custom timeout configuration", async () => {
41
- const configWithTimeout = await createTestConfigAsync({
42
- timeouts: {
43
- pdsMetadata: 60000,
44
- apiRequests: 45000,
45
- },
46
- });
47
- expect(() => new OAuthClient(configWithTimeout)).not.toThrow();
48
- });
49
- });
50
-
51
- describe("authorize", () => {
52
- it("should throw AuthenticationError for invalid identifier", async () => {
53
- const client = new OAuthClient(config);
54
- // Note: This will fail because we don't have a real PDS to connect to
55
- // But we can test that it properly wraps errors
56
- await expect(client.authorize("invalid-handle")).rejects.toThrow(AuthenticationError);
57
- });
58
-
59
- it("should use custom scope if provided", async () => {
60
- const client = new OAuthClient(config);
61
- // This will fail due to network, but we can verify the error handling
62
- await expect(client.authorize("test.bsky.social", { scope: "custom-scope" })).rejects.toThrow();
63
- });
64
- });
65
-
66
- describe("callback", () => {
67
- it("should throw AuthenticationError for OAuth error params", async () => {
68
- const client = new OAuthClient(config);
69
- const params = new URLSearchParams({
70
- error: "access_denied",
71
- error_description: "User denied access",
72
- });
73
-
74
- await expect(client.callback(params)).rejects.toThrow(AuthenticationError);
75
- await expect(client.callback(params)).rejects.toThrow("User denied access");
76
- });
77
-
78
- it("should throw AuthenticationError for missing code", async () => {
79
- const client = new OAuthClient(config);
80
- const params = new URLSearchParams({
81
- state: "test-state",
82
- // Missing 'code' parameter
83
- });
84
-
85
- // This will fail because callback needs valid OAuth params
86
- await expect(client.callback(params)).rejects.toThrow();
87
- });
88
- });
89
-
90
- describe("restore", () => {
91
- it("should return null for non-existent session", async () => {
92
- const client = new OAuthClient(config);
93
- // Use a valid DID format (did:plc needs 32 char base32 suffix)
94
- const validDid = "did:plc:abcdefghijklmnopqrstuvwxyz123456";
95
- // This will fail due to network/DID validation, but we can verify error handling
96
- await expect(client.restore(validDid)).rejects.toThrow();
97
- });
98
-
99
- it("should throw AuthenticationError for invalid DID", async () => {
100
- const client = new OAuthClient(config);
101
- // Invalid DID format
102
- await expect(client.restore("did:plc:test")).rejects.toThrow(AuthenticationError);
103
- });
104
- });
105
-
106
- describe("revoke", () => {
107
- it("should not throw for non-existent session", async () => {
108
- const client = new OAuthClient(config);
109
- // Revoking a non-existent session should not throw
110
- // Use valid DID format - revoke will fail due to network, but error handling is tested
111
- const validDid = "did:plc:abcdefghijklmnopqrstuvwxyz123456";
112
- await expect(client.revoke(validDid)).rejects.toThrow();
113
- });
114
- });
115
-
116
- describe("storage integration", () => {
117
- it("should use provided session store", async () => {
118
- const sessionStore = new InMemorySessionStore();
119
- const configWithStore = await createTestConfigAsync({
120
- storage: {
121
- sessionStore,
122
- stateStore: new InMemoryStateStore(),
123
- },
124
- });
125
-
126
- const client = new OAuthClient(configWithStore);
127
- // Verify client is created with custom store
128
- expect(client).toBeDefined();
129
- // Note: Actual restore will fail due to network/DID validation,
130
- // but the store integration is verified by client creation
131
- });
132
-
133
- it("should use provided state store", async () => {
134
- const stateStore = new InMemoryStateStore();
135
- const configWithStore = await createTestConfigAsync({
136
- storage: {
137
- sessionStore: new InMemorySessionStore(),
138
- stateStore,
139
- },
140
- });
141
-
142
- const client = new OAuthClient(configWithStore);
143
- expect(client).toBeDefined();
144
- });
145
- });
146
-
147
- describe("logger integration", () => {
148
- it("should use provided logger", async () => {
149
- const { MockLogger } = await import("../utils/mocks.js");
150
- const logger = new MockLogger();
151
- const configWithLogger = await createTestConfigAsync({ logger });
152
-
153
- const client = new OAuthClient(configWithLogger);
154
- // Try an operation that should log (will fail but should log)
155
- try {
156
- await client.restore("did:plc:test");
157
- } catch {
158
- // Expected to fail
159
- }
160
- // Logger should have been called during initialization or error handling
161
- expect(logger.logs.length).toBeGreaterThan(0);
162
- });
163
- });
164
- });
@@ -1,176 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "vitest";
2
- import { ATProtoSDK, createATProtoSDK } from "../../src/core/SDK.js";
3
- import { ValidationError } from "../../src/core/errors.js";
4
- import { createTestConfigAsync } from "../utils/fixtures.js";
5
- import { InMemorySessionStore, InMemoryStateStore } from "../utils/mocks.js";
6
-
7
- describe("ATProtoSDK", () => {
8
- let config: Awaited<ReturnType<typeof createTestConfigAsync>>;
9
-
10
- beforeEach(async () => {
11
- config = await createTestConfigAsync();
12
- });
13
-
14
- describe("constructor", () => {
15
- it("should create SDK instance with valid config", () => {
16
- const sdk = new ATProtoSDK(config);
17
- expect(sdk).toBeInstanceOf(ATProtoSDK);
18
- });
19
-
20
- it("should validate config with Zod schema", () => {
21
- const invalidConfig = {
22
- ...config,
23
- oauth: {
24
- ...config.oauth,
25
- clientId: "not-a-url", // Invalid URL
26
- },
27
- };
28
-
29
- expect(() => new ATProtoSDK(invalidConfig)).toThrow(ValidationError);
30
- expect(() => new ATProtoSDK(invalidConfig)).toThrow("Invalid SDK configuration");
31
- });
32
-
33
- it("should accept optional cache", async () => {
34
- const { InMemoryCache } = await import("../utils/mocks.js");
35
- const cache = new InMemoryCache();
36
- const configWithCache = await createTestConfigAsync({ cache });
37
- expect(() => new ATProtoSDK(configWithCache)).not.toThrow();
38
- });
39
-
40
- it("should accept optional logger", async () => {
41
- const { MockLogger } = await import("../utils/mocks.js");
42
- const logger = new MockLogger();
43
- const configWithLogger = await createTestConfigAsync({ logger });
44
- expect(() => new ATProtoSDK(configWithLogger)).not.toThrow();
45
- });
46
-
47
- it("should work without storage (uses in-memory defaults)", async () => {
48
- const config = await createTestConfigAsync();
49
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
- const { storage, ...configWithoutStorage } = config;
51
- // Storage is optional - SDK will use in-memory defaults
52
- const sdk = new ATProtoSDK(configWithoutStorage);
53
- expect(sdk).toBeInstanceOf(ATProtoSDK);
54
- });
55
- });
56
-
57
- describe("createATProtoSDK factory", () => {
58
- it("should create SDK instance", () => {
59
- const sdk = createATProtoSDK(config);
60
- expect(sdk).toBeInstanceOf(ATProtoSDK);
61
- });
62
-
63
- it("should be equivalent to constructor", () => {
64
- const sdk1 = new ATProtoSDK(config);
65
- const sdk2 = createATProtoSDK(config);
66
- expect(sdk1).toBeInstanceOf(ATProtoSDK);
67
- expect(sdk2).toBeInstanceOf(ATProtoSDK);
68
- });
69
- });
70
-
71
- describe("authorize", () => {
72
- it("should throw ValidationError for empty identifier", async () => {
73
- const sdk = new ATProtoSDK(config);
74
- await expect(sdk.authorize("")).rejects.toThrow(ValidationError);
75
- await expect(sdk.authorize(" ")).rejects.toThrow(ValidationError);
76
- });
77
-
78
- it("should trim identifier", async () => {
79
- const sdk = new ATProtoSDK(config);
80
- // Will fail due to network, but should not throw ValidationError
81
- await expect(sdk.authorize(" test.bsky.social ")).rejects.not.toThrow(ValidationError);
82
- });
83
- });
84
-
85
- describe("restoreSession", () => {
86
- it("should throw ValidationError for empty DID", async () => {
87
- const sdk = new ATProtoSDK(config);
88
- await expect(sdk.restoreSession("")).rejects.toThrow(ValidationError);
89
- await expect(sdk.restoreSession(" ")).rejects.toThrow(ValidationError);
90
- });
91
-
92
- it("should handle non-existent session", async () => {
93
- const sdk = new ATProtoSDK(config);
94
- // Use valid DID format - will fail due to network but tests error handling
95
- const validDid = "did:plc:abcdefghijklmnopqrstuvwxyz123456";
96
- // This will fail due to network/DID validation
97
- await expect(sdk.restoreSession(validDid)).rejects.toThrow();
98
- });
99
- });
100
-
101
- describe("revokeSession", () => {
102
- it("should throw ValidationError for empty DID", async () => {
103
- const sdk = new ATProtoSDK(config);
104
- await expect(sdk.revokeSession("")).rejects.toThrow(ValidationError);
105
- await expect(sdk.revokeSession(" ")).rejects.toThrow(ValidationError);
106
- });
107
- });
108
-
109
- describe("repository", () => {
110
- it("should throw ValidationError when session is null", () => {
111
- const sdk = new ATProtoSDK(config);
112
- expect(() => sdk.repository(null as any)).toThrow(ValidationError);
113
- });
114
-
115
- it("should throw ValidationError when PDS not configured and no server specified", async () => {
116
- const configWithoutServers = await createTestConfigAsync();
117
- delete configWithoutServers.servers;
118
- const sdk = new ATProtoSDK(configWithoutServers);
119
- const mockSession = { did: "did:plc:test", sub: "did:plc:test", fetchHandler: async () => new Response() } as any;
120
- expect(() => sdk.repository(mockSession)).toThrow(ValidationError);
121
- });
122
-
123
- it("should throw ValidationError when SDS not configured and server=sds", async () => {
124
- const configWithOnlyPds = await createTestConfigAsync();
125
- configWithOnlyPds.servers = { pds: "https://pds.example.com" };
126
- const sdk = new ATProtoSDK(configWithOnlyPds);
127
- const mockSession = { did: "did:plc:test", sub: "did:plc:test", fetchHandler: async () => new Response() } as any;
128
- expect(() => sdk.repository(mockSession, { server: "sds" })).toThrow(ValidationError);
129
- });
130
-
131
- it("should create repository with custom serverUrl", () => {
132
- const sdk = new ATProtoSDK(config);
133
- const mockSession = { did: "did:plc:test", sub: "did:plc:test", fetchHandler: async () => new Response() } as any;
134
- const repo = sdk.repository(mockSession, { serverUrl: "https://custom.server.com" });
135
- expect(repo).toBeDefined();
136
- expect(repo.getServerUrl()).toBe("https://custom.server.com");
137
- });
138
- });
139
-
140
- describe("setup examples", () => {
141
- it("should work with minimal config (no storage provided)", async () => {
142
- const minimalConfig = await createTestConfigAsync();
143
- // Remove storage to test default in-memory implementation
144
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
145
- const { storage, ...configWithoutStorage } = minimalConfig;
146
- // Storage is optional - SDK will use in-memory defaults
147
- const sdk = createATProtoSDK(configWithoutStorage);
148
- expect(sdk).toBeInstanceOf(ATProtoSDK);
149
- });
150
-
151
- it("should work with all optional fields", async () => {
152
- const { InMemoryCache, MockLogger } = await import("../utils/mocks.js");
153
- const fullConfig = await createTestConfigAsync({
154
- cache: new InMemoryCache(),
155
- logger: new MockLogger(),
156
- timeouts: {
157
- pdsMetadata: 60000,
158
- apiRequests: 45000,
159
- },
160
- });
161
- const sdk = createATProtoSDK(fullConfig);
162
- expect(sdk).toBeInstanceOf(ATProtoSDK);
163
- });
164
-
165
- it("should work with custom storage implementations", async () => {
166
- const customConfig = await createTestConfigAsync({
167
- storage: {
168
- sessionStore: new InMemorySessionStore(),
169
- stateStore: new InMemoryStateStore(),
170
- },
171
- });
172
- const sdk = createATProtoSDK(customConfig);
173
- expect(sdk).toBeInstanceOf(ATProtoSDK);
174
- });
175
- });
176
- });
@@ -1,81 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- ATProtoSDKError,
4
- AuthenticationError,
5
- SessionExpiredError,
6
- ValidationError,
7
- NetworkError,
8
- SDSRequiredError,
9
- } from "../../src/core/errors.js";
10
-
11
- describe("ATProtoSDKError", () => {
12
- it("should create error with message and code", () => {
13
- const error = new ATProtoSDKError("Test error", "TEST_ERROR", 500);
14
- expect(error.message).toBe("Test error");
15
- expect(error.code).toBe("TEST_ERROR");
16
- expect(error.status).toBe(500);
17
- expect(error.name).toBe("ATProtoSDKError");
18
- });
19
-
20
- it("should include cause if provided", () => {
21
- const cause = new Error("Original error");
22
- const error = new ATProtoSDKError("Test error", "TEST_ERROR", 500, cause);
23
- expect(error.cause).toBe(cause);
24
- });
25
- });
26
-
27
- describe("AuthenticationError", () => {
28
- it("should create authentication error with 401 status", () => {
29
- const error = new AuthenticationError("Auth failed");
30
- expect(error.message).toBe("Auth failed");
31
- expect(error.code).toBe("AUTHENTICATION_ERROR");
32
- expect(error.status).toBe(401);
33
- expect(error.name).toBe("AuthenticationError");
34
- });
35
- });
36
-
37
- describe("SessionExpiredError", () => {
38
- it("should create session expired error with default message", () => {
39
- const error = new SessionExpiredError();
40
- expect(error.message).toBe("Session expired");
41
- expect(error.code).toBe("SESSION_EXPIRED");
42
- expect(error.status).toBe(401);
43
- });
44
-
45
- it("should accept custom message", () => {
46
- const error = new SessionExpiredError("Custom message");
47
- expect(error.message).toBe("Custom message");
48
- });
49
- });
50
-
51
- describe("ValidationError", () => {
52
- it("should create validation error with 400 status", () => {
53
- const error = new ValidationError("Invalid input");
54
- expect(error.message).toBe("Invalid input");
55
- expect(error.code).toBe("VALIDATION_ERROR");
56
- expect(error.status).toBe(400);
57
- });
58
- });
59
-
60
- describe("NetworkError", () => {
61
- it("should create network error with 503 status", () => {
62
- const error = new NetworkError("Network failure");
63
- expect(error.message).toBe("Network failure");
64
- expect(error.code).toBe("NETWORK_ERROR");
65
- expect(error.status).toBe(503);
66
- });
67
- });
68
-
69
- describe("SDSRequiredError", () => {
70
- it("should create SDS required error with default message", () => {
71
- const error = new SDSRequiredError();
72
- expect(error.message).toBe("This operation requires a Shared Data Server (SDS)");
73
- expect(error.code).toBe("SDS_REQUIRED");
74
- expect(error.status).toBe(400);
75
- });
76
-
77
- it("should accept custom message", () => {
78
- const error = new SDSRequiredError("Custom SDS error");
79
- expect(error.message).toBe("Custom SDS error");
80
- });
81
- });
@@ -1,155 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { BlobOperationsImpl } from "../../src/repository/BlobOperationsImpl.js";
3
- import { NetworkError } from "../../src/core/errors.js";
4
-
5
- describe("BlobOperationsImpl", () => {
6
- let mockAgent: any;
7
- let blobOps: BlobOperationsImpl;
8
- const repoDid = "did:plc:testdid123";
9
- const serverUrl = "https://pds.example.com";
10
-
11
- beforeEach(() => {
12
- mockAgent = {
13
- com: {
14
- atproto: {
15
- repo: {
16
- uploadBlob: vi.fn(),
17
- },
18
- sync: {
19
- getBlob: vi.fn(),
20
- },
21
- },
22
- },
23
- };
24
-
25
- blobOps = new BlobOperationsImpl(mockAgent, repoDid, serverUrl);
26
- });
27
-
28
- describe("upload", () => {
29
- it("should upload a blob successfully", async () => {
30
- const mockBlob = new Blob(["test content"], { type: "text/plain" });
31
- const mockCID = {
32
- toString: () => "bafyrei123",
33
- };
34
- mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
35
- success: true,
36
- data: {
37
- blob: {
38
- ref: mockCID,
39
- mimeType: "text/plain",
40
- size: 12,
41
- },
42
- },
43
- });
44
-
45
- const result = await blobOps.upload(mockBlob);
46
-
47
- expect(result.ref).toEqual({ $link: "bafyrei123" });
48
- expect(result.mimeType).toBe("text/plain");
49
- expect(result.size).toBe(12);
50
- });
51
-
52
- it("should use blob type as encoding", async () => {
53
- const mockBlob = new Blob(["image data"], { type: "image/png" });
54
- mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
55
- success: true,
56
- data: {
57
- blob: {
58
- ref: { $link: "bafyrei123" },
59
- mimeType: "image/png",
60
- size: 100,
61
- },
62
- },
63
- });
64
-
65
- await blobOps.upload(mockBlob);
66
-
67
- expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(expect.any(Uint8Array), {
68
- encoding: "image/png",
69
- });
70
- });
71
-
72
- it("should default to application/octet-stream for blobs without type", async () => {
73
- const mockBlob = new Blob(["data"]);
74
- mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
75
- success: true,
76
- data: {
77
- blob: {
78
- ref: { $link: "bafyrei123" },
79
- mimeType: "application/octet-stream",
80
- size: 4,
81
- },
82
- },
83
- });
84
-
85
- await blobOps.upload(mockBlob);
86
-
87
- expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(expect.any(Uint8Array), {
88
- encoding: "application/octet-stream",
89
- });
90
- });
91
-
92
- it("should throw NetworkError when API returns success: false", async () => {
93
- const mockBlob = new Blob(["test"]);
94
- mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
95
- success: false,
96
- });
97
-
98
- await expect(blobOps.upload(mockBlob)).rejects.toThrow(NetworkError);
99
- });
100
-
101
- it("should throw NetworkError when API throws", async () => {
102
- const mockBlob = new Blob(["test"]);
103
- mockAgent.com.atproto.repo.uploadBlob.mockRejectedValue(new Error("Upload failed"));
104
-
105
- await expect(blobOps.upload(mockBlob)).rejects.toThrow(NetworkError);
106
- });
107
- });
108
-
109
- describe("get", () => {
110
- it("should get a blob successfully", async () => {
111
- const mockData = new Uint8Array([1, 2, 3, 4]);
112
- mockAgent.com.atproto.sync.getBlob.mockResolvedValue({
113
- success: true,
114
- data: mockData,
115
- headers: { "content-type": "image/png" },
116
- });
117
-
118
- const result = await blobOps.get("bafyrei123");
119
-
120
- expect(result.data).toEqual(mockData);
121
- expect(result.mimeType).toBe("image/png");
122
- expect(mockAgent.com.atproto.sync.getBlob).toHaveBeenCalledWith({
123
- did: repoDid,
124
- cid: "bafyrei123",
125
- });
126
- });
127
-
128
- it("should default to application/octet-stream if no content-type header", async () => {
129
- const mockData = new Uint8Array([1, 2, 3]);
130
- mockAgent.com.atproto.sync.getBlob.mockResolvedValue({
131
- success: true,
132
- data: mockData,
133
- headers: {},
134
- });
135
-
136
- const result = await blobOps.get("bafyrei123");
137
-
138
- expect(result.mimeType).toBe("application/octet-stream");
139
- });
140
-
141
- it("should throw NetworkError when API returns success: false", async () => {
142
- mockAgent.com.atproto.sync.getBlob.mockResolvedValue({
143
- success: false,
144
- });
145
-
146
- await expect(blobOps.get("bafyrei123")).rejects.toThrow(NetworkError);
147
- });
148
-
149
- it("should throw NetworkError when API throws", async () => {
150
- mockAgent.com.atproto.sync.getBlob.mockRejectedValue(new Error("Blob not found"));
151
-
152
- await expect(blobOps.get("bafyrei123")).rejects.toThrow(NetworkError);
153
- });
154
- });
155
- });