import { expect, tap } from '@git.zone/tstest/tapbundle'; import { SmartRegistry } from '../ts/index.js'; import { createTestRegistry, createTestTokens, createRubyGem, calculateRubyGemsChecksums, } from './helpers/registry.js'; let registry: SmartRegistry; let rubygemsToken: string; let userId: string; // Test data const testGemName = 'test-gem'; const testVersion = '1.0.0'; let testGemData: Buffer; tap.test('RubyGems: should create registry instance', async () => { registry = await createTestRegistry(); const tokens = await createTestTokens(registry); rubygemsToken = tokens.rubygemsToken; userId = tokens.userId; expect(registry).toBeInstanceOf(SmartRegistry); expect(rubygemsToken).toBeTypeOf('string'); // Clean up any existing metadata from previous test runs const storage = registry.getStorage(); try { await storage.deleteRubyGem(testGemName); } catch (error) { // Ignore error if gem doesn't exist } }); tap.test('RubyGems: should create test gem file', async () => { testGemData = await createRubyGem(testGemName, testVersion); expect(testGemData).toBeInstanceOf(Buffer); expect(testGemData.length).toBeGreaterThan(0); }); tap.test('RubyGems: should upload gem file (POST /rubygems/api/v1/gems)', async () => { const response = await registry.handleRequest({ method: 'POST', path: '/rubygems/api/v1/gems', headers: { Authorization: rubygemsToken, 'Content-Type': 'application/octet-stream', }, query: {}, body: testGemData, }); expect(response.status).toEqual(201); expect(response.body).toHaveProperty('message'); }); tap.test('RubyGems: should retrieve Compact Index versions file (GET /rubygems/versions)', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('text/plain; charset=utf-8'); expect(response.body).toBeInstanceOf(Buffer); const content = (response.body as Buffer).toString('utf-8'); expect(content).toContain('created_at:'); expect(content).toContain('---'); expect(content).toContain(testGemName); expect(content).toContain(testVersion); }); tap.test('RubyGems: should retrieve Compact Index info file (GET /rubygems/info/{gem})', async () => { const response = await registry.handleRequest({ method: 'GET', path: `/rubygems/info/${testGemName}`, headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('text/plain; charset=utf-8'); expect(response.body).toBeInstanceOf(Buffer); const content = (response.body as Buffer).toString('utf-8'); expect(content).toContain('---'); expect(content).toContain(testVersion); expect(content).toContain('checksum:'); }); tap.test('RubyGems: should retrieve Compact Index names file (GET /rubygems/names)', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/names', headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('text/plain; charset=utf-8'); expect(response.body).toBeInstanceOf(Buffer); const content = (response.body as Buffer).toString('utf-8'); expect(content).toContain('---'); expect(content).toContain(testGemName); }); tap.test('RubyGems: should download gem file (GET /rubygems/gems/{gem}-{version}.gem)', async () => { const response = await registry.handleRequest({ method: 'GET', path: `/rubygems/gems/${testGemName}-${testVersion}.gem`, headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.body).toBeInstanceOf(Buffer); expect((response.body as Buffer).length).toEqual(testGemData.length); expect(response.headers['Content-Type']).toEqual('application/octet-stream'); }); tap.test('RubyGems: should upload a second version', async () => { const newVersion = '2.0.0'; const newGemData = await createRubyGem(testGemName, newVersion); const response = await registry.handleRequest({ method: 'POST', path: '/rubygems/api/v1/gems', headers: { Authorization: rubygemsToken, 'Content-Type': 'application/octet-stream', }, query: {}, body: newGemData, }); expect(response.status).toEqual(201); }); tap.test('RubyGems: should list multiple versions in Compact Index', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: {}, query: {}, }); expect(response.status).toEqual(200); const content = (response.body as Buffer).toString('utf-8'); const lines = content.split('\n'); const gemLine = lines.find(l => l.startsWith(`${testGemName} `)); expect(gemLine).toBeDefined(); expect(gemLine).toContain('1.0.0'); expect(gemLine).toContain('2.0.0'); }); tap.test('RubyGems: should list multiple versions in info file', async () => { const response = await registry.handleRequest({ method: 'GET', path: `/rubygems/info/${testGemName}`, headers: {}, query: {}, }); expect(response.status).toEqual(200); const content = (response.body as Buffer).toString('utf-8'); expect(content).toContain('1.0.0'); expect(content).toContain('2.0.0'); }); tap.test('RubyGems: should support platform-specific gems', async () => { const platformVersion = '1.5.0'; const platform = 'x86_64-linux'; const platformGemData = await createRubyGem(testGemName, platformVersion, platform); const response = await registry.handleRequest({ method: 'POST', path: '/rubygems/api/v1/gems', headers: { Authorization: rubygemsToken, 'Content-Type': 'application/octet-stream', }, query: {}, body: platformGemData, }); expect(response.status).toEqual(201); // Verify platform is listed in versions const versionsResponse = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: {}, query: {}, }); const content = (versionsResponse.body as Buffer).toString('utf-8'); const lines = content.split('\n'); const gemLine = lines.find(l => l.startsWith(`${testGemName} `)); expect(gemLine).toContain(`${platformVersion}_${platform}`); }); tap.test('RubyGems: should yank a gem version (DELETE /rubygems/api/v1/gems/yank)', async () => { const response = await registry.handleRequest({ method: 'DELETE', path: '/rubygems/api/v1/gems/yank', headers: { Authorization: rubygemsToken, }, query: { gem_name: testGemName, version: testVersion, }, }); expect(response.status).toEqual(200); expect(response.body).toHaveProperty('message'); expect((response.body as any).message).toContain('yanked'); }); tap.test('RubyGems: should mark yanked version in Compact Index', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: {}, query: {}, }); expect(response.status).toEqual(200); const content = (response.body as Buffer).toString('utf-8'); const lines = content.split('\n'); const gemLine = lines.find(l => l.startsWith(`${testGemName} `)); // Yanked versions are prefixed with '-' expect(gemLine).toContain(`-${testVersion}`); }); tap.test('RubyGems: should still allow downloading yanked gem', async () => { // Yanked gems can still be downloaded if explicitly requested const response = await registry.handleRequest({ method: 'GET', path: `/rubygems/gems/${testGemName}-${testVersion}.gem`, headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.body).toBeInstanceOf(Buffer); }); tap.test('RubyGems: should unyank a gem version (PUT /rubygems/api/v1/gems/unyank)', async () => { const response = await registry.handleRequest({ method: 'PUT', path: '/rubygems/api/v1/gems/unyank', headers: { Authorization: rubygemsToken, }, query: { gem_name: testGemName, version: testVersion, }, }); expect(response.status).toEqual(200); expect(response.body).toHaveProperty('message'); expect((response.body as any).message).toContain('unyanked'); }); tap.test('RubyGems: should remove yank marker after unyank', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: {}, query: {}, }); expect(response.status).toEqual(200); const content = (response.body as Buffer).toString('utf-8'); const lines = content.split('\n'); const gemLine = lines.find(l => l.startsWith(`${testGemName} `)); // After unyank, version should not have '-' prefix const versions = gemLine!.split(' ')[1].split(','); const version1 = versions.find(v => v.includes('1.0.0')); expect(version1).not.toStartWith('-'); expect(version1).toContain('1.0.0'); }); tap.test('RubyGems: should retrieve versions JSON (GET /rubygems/api/v1/versions/{gem}.json)', async () => { const response = await registry.handleRequest({ method: 'GET', path: `/rubygems/api/v1/versions/${testGemName}.json`, headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('application/json'); expect(response.body).toBeTypeOf('object'); const json = response.body as any; expect(json).toHaveProperty('name'); expect(json.name).toEqual(testGemName); expect(json).toHaveProperty('versions'); expect(json.versions).toBeTypeOf('object'); expect(json.versions.length).toBeGreaterThan(0); }); tap.test('RubyGems: should retrieve dependencies JSON (GET /rubygems/api/v1/dependencies)', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/api/v1/dependencies', headers: {}, query: { gems: `${testGemName}`, }, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('application/json'); expect(response.body).toBeTypeOf('object'); const json = response.body as any; expect(Array.isArray(json)).toEqual(true); }); tap.test('RubyGems: should retrieve gem spec (GET /rubygems/quick/Marshal.4.8/{gem}-{version}.gemspec.rz)', async () => { const response = await registry.handleRequest({ method: 'GET', path: `/rubygems/quick/Marshal.4.8/${testGemName}-${testVersion}.gemspec.rz`, headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.body).toBeInstanceOf(Buffer); }); tap.test('RubyGems: should support latest specs endpoint (GET /rubygems/latest_specs.4.8.gz)', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/latest_specs.4.8.gz', headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('application/octet-stream'); expect(response.body).toBeInstanceOf(Buffer); }); tap.test('RubyGems: should support specs endpoint (GET /rubygems/specs.4.8.gz)', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/specs.4.8.gz', headers: {}, query: {}, }); expect(response.status).toEqual(200); expect(response.headers['Content-Type']).toEqual('application/octet-stream'); expect(response.body).toBeInstanceOf(Buffer); }); tap.test('RubyGems: should return 404 for non-existent gem', async () => { const response = await registry.handleRequest({ method: 'GET', path: '/rubygems/gems/nonexistent-gem-1.0.0.gem', headers: {}, query: {}, }); expect(response.status).toEqual(404); expect(response.body).toHaveProperty('error'); }); tap.test('RubyGems: should return 401 for unauthorized upload', async () => { const gemData = await createRubyGem('unauthorized-gem', '1.0.0'); const response = await registry.handleRequest({ method: 'POST', path: '/rubygems/api/v1/gems', headers: { // No authorization header 'Content-Type': 'application/octet-stream', }, query: {}, body: gemData, }); expect(response.status).toEqual(401); expect(response.body).toHaveProperty('error'); }); tap.test('RubyGems: should return 401 for unauthorized yank', async () => { const response = await registry.handleRequest({ method: 'DELETE', path: '/rubygems/api/v1/gems/yank', headers: { // No authorization header }, query: { gem_name: testGemName, version: '2.0.0', }, }); expect(response.status).toEqual(401); expect(response.body).toHaveProperty('error'); }); tap.test('RubyGems: should handle gem with dependencies', async () => { const gemWithDeps = 'gem-with-deps'; const version = '1.0.0'; const gemData = await createRubyGem(gemWithDeps, version); const response = await registry.handleRequest({ method: 'POST', path: '/rubygems/api/v1/gems', headers: { Authorization: rubygemsToken, 'Content-Type': 'application/octet-stream', }, query: {}, body: gemData, }); expect(response.status).toEqual(201); // Check info file contains dependency info const infoResponse = await registry.handleRequest({ method: 'GET', path: `/rubygems/info/${gemWithDeps}`, headers: {}, query: {}, }); expect(infoResponse.status).toEqual(200); const content = (infoResponse.body as Buffer).toString('utf-8'); expect(content).toContain('checksum:'); }); tap.test('RubyGems: should validate gem filename format', async () => { const invalidGemData = Buffer.from('invalid gem data'); const response = await registry.handleRequest({ method: 'POST', path: '/rubygems/api/v1/gems', headers: { Authorization: rubygemsToken, 'Content-Type': 'application/octet-stream', }, query: {}, body: invalidGemData, }); // Should fail validation expect(response.status).toBeGreaterThanOrEqual(400); }); tap.test('RubyGems: should support conditional GET with ETag', async () => { // First request to get ETag const response1 = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: {}, query: {}, }); const etag = response1.headers['ETag']; expect(etag).toBeDefined(); // Second request with If-None-Match const response2 = await registry.handleRequest({ method: 'GET', path: '/rubygems/versions', headers: { 'If-None-Match': etag as string, }, query: {}, }); expect(response2.status).toEqual(304); }); tap.postTask('cleanup registry', async () => { if (registry) { registry.destroy(); } }); export default tap.start();