@opentermsarchive/engine 15.3.1 → 16.0.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.
@@ -0,0 +1,209 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ import { expect, use } from 'chai';
6
+ import chaiAsPromised from 'chai-as-promised';
7
+ import sinon from 'sinon';
8
+
9
+ import DatasetStorage, { TEMPORARY_SUFFIX } from './storage.js';
10
+
11
+ use(chaiAsPromised);
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ const TMP_PATH = path.resolve(__dirname, './tmp');
16
+ const ARCHIVE_FILENAME = 'dataset-2026-01-01.zip';
17
+ const PREVIOUS_ARCHIVE_FILENAME = 'dataset-2025-12-25.zip';
18
+ const INTERRUPTED_ARCHIVE_FILENAME = `dataset-2025-12-26.zip${TEMPORARY_SUFFIX}`;
19
+ const UNRELATED_FILENAME = 'notes.txt';
20
+ const METADATA = {
21
+ filename: ARCHIVE_FILENAME,
22
+ title: 'sandbox',
23
+ license: 'ODbL-1.0',
24
+ releaseDate: '2026-01-01T08:30:00Z',
25
+ firstVersionDate: '2021-01-01T11:27:00Z',
26
+ lastVersionDate: '2022-01-06T11:32:47Z',
27
+ servicesCount: 2,
28
+ termsCount: 3,
29
+ versionsCount: 4,
30
+ size: 15,
31
+ sha256: 'a'.repeat(64),
32
+ };
33
+
34
+ describe('DatasetStorage', () => {
35
+ const storage = new DatasetStorage(TMP_PATH);
36
+
37
+ async function removeStorage() {
38
+ await fs.rm(TMP_PATH, { recursive: true, force: true });
39
+ }
40
+
41
+ before(removeStorage);
42
+
43
+ describe('#findLatest', () => {
44
+ context('when the storage directory does not exist', () => {
45
+ it('returns null', async () => {
46
+ expect(await storage.findLatest()).to.be.null;
47
+ });
48
+ });
49
+
50
+ context('when no metadata file exists', () => {
51
+ before(async () => {
52
+ await fs.mkdir(TMP_PATH, { recursive: true });
53
+ });
54
+
55
+ after(removeStorage);
56
+
57
+ it('returns null', async () => {
58
+ expect(await storage.findLatest()).to.be.null;
59
+ });
60
+ });
61
+
62
+ context('when the metadata file references a missing archive', () => {
63
+ before(async () => {
64
+ await fs.mkdir(TMP_PATH, { recursive: true });
65
+ await fs.writeFile(storage.metadataPath, JSON.stringify(METADATA));
66
+ });
67
+
68
+ after(removeStorage);
69
+
70
+ it('returns null', async () => {
71
+ expect(await storage.findLatest()).to.be.null;
72
+ });
73
+ });
74
+
75
+ context('when the metadata file is corrupted', () => {
76
+ before(async () => {
77
+ await fs.mkdir(TMP_PATH, { recursive: true });
78
+ await fs.writeFile(storage.metadataPath, '{ not json');
79
+ });
80
+
81
+ after(removeStorage);
82
+
83
+ it('rejects', async () => {
84
+ await expect(storage.findLatest()).to.be.rejected;
85
+ });
86
+ });
87
+
88
+ context('when the metadata file and the archive exist', () => {
89
+ before(async () => {
90
+ await fs.mkdir(TMP_PATH, { recursive: true });
91
+ await fs.writeFile(storage.archivePath(ARCHIVE_FILENAME), 'archive content');
92
+ await fs.writeFile(storage.metadataPath, JSON.stringify(METADATA));
93
+ });
94
+
95
+ after(removeStorage);
96
+
97
+ it('returns the metadata', async () => {
98
+ expect(await storage.findLatest()).to.deep.equal(METADATA);
99
+ });
100
+ });
101
+ });
102
+
103
+ describe('#save', () => {
104
+ context('when the archive is missing', () => {
105
+ before(async () => {
106
+ await fs.mkdir(TMP_PATH, { recursive: true });
107
+ });
108
+
109
+ after(removeStorage);
110
+
111
+ it('rejects', async () => {
112
+ await expect(storage.save(METADATA)).to.be.rejected;
113
+ });
114
+
115
+ it('writes no metadata file', async () => {
116
+ await storage.save(METADATA).catch(() => {});
117
+
118
+ await expect(fs.access(storage.metadataPath)).to.be.rejected;
119
+ });
120
+ });
121
+
122
+ context('when the archive exists', () => {
123
+ before(async () => {
124
+ await fs.mkdir(TMP_PATH, { recursive: true });
125
+ await fs.writeFile(storage.archivePath(ARCHIVE_FILENAME), 'archive content');
126
+ await storage.save(METADATA);
127
+ });
128
+
129
+ after(removeStorage);
130
+
131
+ it('writes the metadata file', async () => {
132
+ expect(JSON.parse(await fs.readFile(storage.metadataPath, 'utf8'))).to.deep.equal(METADATA);
133
+ });
134
+
135
+ it('leaves no temporary metadata file', async () => {
136
+ await expect(fs.access(`${storage.metadataPath}${TEMPORARY_SUFFIX}`)).to.be.rejected;
137
+ });
138
+
139
+ it('keeps the archive', async () => {
140
+ await expect(fs.access(storage.archivePath(ARCHIVE_FILENAME))).to.be.fulfilled;
141
+ });
142
+ });
143
+ });
144
+
145
+ describe('#removePreviousArchives', () => {
146
+ context('when no metadata exists', () => {
147
+ before(async () => {
148
+ await fs.mkdir(TMP_PATH, { recursive: true });
149
+ });
150
+
151
+ after(removeStorage);
152
+
153
+ it('does nothing', async () => {
154
+ await expect(storage.removePreviousArchives()).to.be.fulfilled;
155
+ });
156
+ });
157
+
158
+ context('when metadata exists', () => {
159
+ before(async () => {
160
+ await fs.mkdir(TMP_PATH, { recursive: true });
161
+ await fs.writeFile(storage.archivePath(ARCHIVE_FILENAME), 'archive content');
162
+ await fs.writeFile(storage.archivePath(PREVIOUS_ARCHIVE_FILENAME), 'previous archive');
163
+ await fs.writeFile(storage.archivePath(INTERRUPTED_ARCHIVE_FILENAME), 'interrupted archive');
164
+ await fs.writeFile(storage.archivePath(UNRELATED_FILENAME), 'unrelated content');
165
+ await fs.writeFile(storage.metadataPath, JSON.stringify(METADATA));
166
+
167
+ await storage.removePreviousArchives();
168
+ });
169
+
170
+ after(removeStorage);
171
+
172
+ it('keeps the current archive', async () => {
173
+ await expect(fs.access(storage.archivePath(ARCHIVE_FILENAME))).to.be.fulfilled;
174
+ });
175
+
176
+ it('removes previous archives', async () => {
177
+ await expect(fs.access(storage.archivePath(PREVIOUS_ARCHIVE_FILENAME))).to.be.rejected;
178
+ });
179
+
180
+ it('removes interrupted archives', async () => {
181
+ await expect(fs.access(storage.archivePath(INTERRUPTED_ARCHIVE_FILENAME))).to.be.rejected;
182
+ });
183
+
184
+ it('keeps unrelated files', async () => {
185
+ await expect(fs.access(storage.archivePath(UNRELATED_FILENAME))).to.be.fulfilled;
186
+ });
187
+ });
188
+
189
+ context('when a previous archive fails to be removed', () => {
190
+ before(async () => {
191
+ await fs.mkdir(TMP_PATH, { recursive: true });
192
+ await fs.writeFile(storage.archivePath(ARCHIVE_FILENAME), 'archive content');
193
+ await fs.writeFile(storage.archivePath(PREVIOUS_ARCHIVE_FILENAME), 'previous archive');
194
+ await fs.writeFile(storage.metadataPath, JSON.stringify(METADATA));
195
+
196
+ sinon.stub(fs, 'rm').rejects(new Error('Permission denied'));
197
+ });
198
+
199
+ after(async () => {
200
+ sinon.restore(); // Restored first, so that the cleanup below runs the real `fs.rm`
201
+ await removeStorage();
202
+ });
203
+
204
+ it('rejects', async () => {
205
+ await expect(storage.removePreviousArchives()).to.be.rejected;
206
+ });
207
+ });
208
+ });
209
+ });