Skip to content

Commit 5f549f6

Browse files
feat(SITES-43695): add defaultSiteId to Organization model and resolveSite endpoint
- Expose defaultSiteId in OrganizationDto.toJSON() for API responses - Add PATCH /organizations/:id support for defaultSiteId (valid UUID or null) - In resolveSite(), check org.defaultSiteId before falling back to getFirstEnrollment() so customers can pin a preferred domain without DB insertion-order dependence - Cross-org and enrollment/tier validation guard the new resolution path - OpenAPI schema updated to document the new field Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8bb8431 commit 5f549f6

6 files changed

Lines changed: 191 additions & 0 deletions

File tree

docs/openapi/schemas.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ Organization:
385385
imsOrgId:
386386
description: Optional. The ID of the Adobe IMS organization
387387
$ref: '#/ImsOrganizationId'
388+
defaultSiteId:
389+
description: Optional. The ID of the default site to resolve on login. When set, /sites-resolve returns this site instead of the insertion-order fallback.
390+
$ref: '#/Id'
388391
config:
389392
description: Optional. The configuration of the organization
390393
$ref: '#/OrganizationConfig'

src/controllers/organizations.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,15 @@ function OrganizationsController(ctx, env) {
331331
updates = true;
332332
}
333333

334+
if ('defaultSiteId' in requestBody) {
335+
const { defaultSiteId } = requestBody;
336+
if (defaultSiteId !== null && defaultSiteId !== undefined && !isValidUUID(defaultSiteId)) {
337+
return badRequest('Invalid defaultSiteId: must be a valid UUID or null');
338+
}
339+
organization.setDefaultSiteId(defaultSiteId ?? undefined);
340+
updates = true;
341+
}
342+
334343
if (updates) {
335344
const updatedOrganization = await organization.save();
336345
return ok(OrganizationDto.toJSON(updatedOrganization));

src/controllers/sites.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,9 +1208,43 @@ function SitesController(ctx, log, env) {
12081208
}
12091209
}
12101210

1211+
// Resolves the org's defaultSiteId if set, validating it belongs to the org and is enrolled.
1212+
// Returns the resolved data object or null to fall through to getFirstEnrollment().
1213+
const resolveOrgDefaultSite = async (org) => {
1214+
const defaultSiteId = org.getDefaultSiteId?.();
1215+
if (!hasText(defaultSiteId) || !isValidUUID(defaultSiteId)) {
1216+
return null;
1217+
}
1218+
1219+
const defaultSite = await Site.findById(defaultSiteId);
1220+
if (!defaultSite || defaultSite.getOrganizationId() !== org.getId()) {
1221+
return null;
1222+
}
1223+
1224+
const siteTierClient = await TierClient.createForSite(context, defaultSite, productCode);
1225+
const { entitlement, enrollments } = await siteTierClient.getAllEnrollment();
1226+
1227+
const tierVisible = entitlement && CUSTOMER_VISIBLE_TIERS.includes(entitlement.getTier());
1228+
if (!tierVisible || !enrollments?.length) {
1229+
return null;
1230+
}
1231+
1232+
const isSummitPlgEnabled = await getIsSummitPlgEnabled(defaultSite, context);
1233+
return {
1234+
organization: OrganizationDto.toJSON(org),
1235+
site: SiteDto.toJSON(defaultSite),
1236+
isSummitPlgEnabled,
1237+
};
1238+
};
1239+
12111240
if (hasText(organizationId) && isValidUUID(organizationId)) {
12121241
organization = await Organization.findById(organizationId);
12131242
if (organization && await accessControlUtil.hasAccess(organization)) {
1243+
const defaultData = await resolveOrgDefaultSite(organization);
1244+
if (defaultData) {
1245+
return ok({ data: defaultData });
1246+
}
1247+
12141248
const tierClient = TierClient.createForOrg(context, organization, productCode);
12151249
const { entitlement: orgEntitlement, site: enrolledSite } = await tierClient
12161250
.getFirstEnrollment();
@@ -1230,6 +1264,11 @@ function SitesController(ctx, log, env) {
12301264
} else if (hasText(imsOrg)) {
12311265
organization = await Organization.findByImsOrgId(imsOrg);
12321266
if (organization && await accessControlUtil.hasAccess(organization)) {
1267+
const defaultData = await resolveOrgDefaultSite(organization);
1268+
if (defaultData) {
1269+
return ok({ data: defaultData });
1270+
}
1271+
12331272
const tierClient = TierClient.createForOrg(context, organization, productCode);
12341273
const { entitlement: imsOrgEntitlement, site: enrolledSite } = await tierClient
12351274
.getFirstEnrollment();

src/dto/organization.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const OrganizationDto = {
2626
id: organization.getId(),
2727
name: organization.getName(),
2828
imsOrgId: organization.getImsOrgId(),
29+
defaultSiteId: organization.getDefaultSiteId(),
2930
createdAt: organization.getCreatedAt(),
3031
updatedAt: organization.getUpdatedAt(),
3132
config: Config.toDynamoItem(organization.getConfig()),

test/controllers/organizations.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,57 @@ describe('Organizations Controller', () => {
439439
expect(error).to.have.property('message', 'No updates provided');
440440
});
441441

442+
it('updates organization defaultSiteId with a valid UUID', async () => {
443+
const validSiteId = '550e8400-e29b-41d4-a716-446655440001';
444+
organizations[0].save = sinon.stub().resolves(organizations[0]);
445+
organizations[0].setDefaultSiteId = sinon.stub();
446+
organizations[0].getDefaultSiteId = sinon.stub().returns(validSiteId);
447+
mockDataAccess.Organization.findById.resolves(organizations[0]);
448+
449+
const response = await organizationsController.updateOrganization({
450+
params: { organizationId: '9033554c-de8a-44ac-a356-09b51af8cc28' },
451+
data: { defaultSiteId: validSiteId },
452+
...context,
453+
});
454+
455+
expect(organizations[0].setDefaultSiteId).to.have.been.calledWith(validSiteId);
456+
expect(organizations[0].save).to.have.been.calledOnce;
457+
expect(response.status).to.equal(200);
458+
});
459+
460+
it('clears organization defaultSiteId when null is provided', async () => {
461+
organizations[0].save = sinon.stub().resolves(organizations[0]);
462+
organizations[0].setDefaultSiteId = sinon.stub();
463+
organizations[0].getDefaultSiteId = sinon.stub().returns(undefined);
464+
mockDataAccess.Organization.findById.resolves(organizations[0]);
465+
466+
const response = await organizationsController.updateOrganization({
467+
params: { organizationId: '9033554c-de8a-44ac-a356-09b51af8cc28' },
468+
data: { defaultSiteId: null },
469+
...context,
470+
});
471+
472+
expect(organizations[0].setDefaultSiteId).to.have.been.calledWith(undefined);
473+
expect(organizations[0].save).to.have.been.calledOnce;
474+
expect(response.status).to.equal(200);
475+
});
476+
477+
it('returns bad request when updating defaultSiteId with an invalid UUID', async () => {
478+
organizations[0].save = sinon.stub().resolves(organizations[0]);
479+
mockDataAccess.Organization.findById.resolves(organizations[0]);
480+
481+
const response = await organizationsController.updateOrganization({
482+
params: { organizationId: '9033554c-de8a-44ac-a356-09b51af8cc28' },
483+
data: { defaultSiteId: 'not-a-uuid' },
484+
...context,
485+
});
486+
487+
expect(organizations[0].save).to.not.have.been.called;
488+
expect(response.status).to.equal(400);
489+
const error = await response.json();
490+
expect(error).to.have.property('message', 'Invalid defaultSiteId: must be a valid UUID or null');
491+
});
492+
442493
it('gets all organizations', async () => {
443494
mockDataAccess.Organization.all.resolves(organizations);
444495

test/controllers/sites.test.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5087,6 +5087,94 @@ describe('Sites Controller', () => {
50875087
const body = await response.json();
50885088
expect(body.message).to.include('No site found for the provided parameters');
50895089
});
5090+
5091+
describe('defaultSiteId resolution', () => {
5092+
it('should resolve defaultSiteId for organizationId path and skip getFirstEnrollment', async () => {
5093+
sandbox.stub(testOrganizations[0], 'getDefaultSiteId').returns(SITE_IDS[0]);
5094+
context.data = { organizationId: testOrganizations[0].getId() };
5095+
mockDataAccess.Organization.findById.resolves(testOrganizations[0]);
5096+
mockDataAccess.Site.findById.resolves(testSites[0]);
5097+
mockTierClientStub.getAllEnrollment.resolves({
5098+
entitlement: { getTier: () => 'FREE_TRIAL' },
5099+
enrollments: [{ getId: () => 'enrollment-1' }],
5100+
});
5101+
5102+
const response = await sitesController.resolveSite(context);
5103+
5104+
expect(response.status).to.equal(200);
5105+
const body = await response.json();
5106+
expect(body.data.site.id).to.equal(SITE_IDS[0]);
5107+
expect(mockTierClientStub.getFirstEnrollment).to.not.have.been.called;
5108+
});
5109+
5110+
it('should resolve defaultSiteId for imsOrg path and skip getFirstEnrollment', async () => {
5111+
sandbox.stub(testOrganizations[0], 'getDefaultSiteId').returns(SITE_IDS[0]);
5112+
context.data = { imsOrg: testOrganizations[0].getImsOrgId() };
5113+
mockDataAccess.Organization.findByImsOrgId.resolves(testOrganizations[0]);
5114+
mockDataAccess.Site.findById.resolves(testSites[0]);
5115+
mockTierClientStub.getAllEnrollment.resolves({
5116+
entitlement: { getTier: () => 'FREE_TRIAL' },
5117+
enrollments: [{ getId: () => 'enrollment-1' }],
5118+
});
5119+
5120+
const response = await sitesController.resolveSite(context);
5121+
5122+
expect(response.status).to.equal(200);
5123+
const body = await response.json();
5124+
expect(body.data.site.id).to.equal(SITE_IDS[0]);
5125+
expect(mockTierClientStub.getFirstEnrollment).to.not.have.been.called;
5126+
});
5127+
5128+
it('should fall through to getFirstEnrollment if defaultSiteId site does not belong to org', async () => {
5129+
// testSites[1] belongs to testOrganizations[3], not testOrganizations[0]
5130+
sandbox.stub(testOrganizations[0], 'getDefaultSiteId').returns(SITE_IDS[1]);
5131+
context.data = { organizationId: testOrganizations[0].getId() };
5132+
mockDataAccess.Organization.findById.resolves(testOrganizations[0]);
5133+
mockDataAccess.Site.findById.resolves(testSites[1]);
5134+
mockTierClientStub.getFirstEnrollment.resolves({
5135+
entitlement: { getTier: () => 'FREE_TRIAL' },
5136+
site: testSites[0],
5137+
});
5138+
5139+
await sitesController.resolveSite(context);
5140+
5141+
expect(mockTierClientStub.getFirstEnrollment).to.have.been.called;
5142+
});
5143+
5144+
it('should fall through to getFirstEnrollment if defaultSiteId site is not found', async () => {
5145+
sandbox.stub(testOrganizations[0], 'getDefaultSiteId').returns(SITE_IDS[0]);
5146+
context.data = { organizationId: testOrganizations[0].getId() };
5147+
mockDataAccess.Organization.findById.resolves(testOrganizations[0]);
5148+
mockDataAccess.Site.findById.resolves(null);
5149+
mockTierClientStub.getFirstEnrollment.resolves({
5150+
entitlement: { getTier: () => 'FREE_TRIAL' },
5151+
site: testSites[0],
5152+
});
5153+
5154+
await sitesController.resolveSite(context);
5155+
5156+
expect(mockTierClientStub.getFirstEnrollment).to.have.been.called;
5157+
});
5158+
5159+
it('should fall through to getFirstEnrollment if defaultSiteId site has no valid enrollment', async () => {
5160+
sandbox.stub(testOrganizations[0], 'getDefaultSiteId').returns(SITE_IDS[0]);
5161+
context.data = { organizationId: testOrganizations[0].getId() };
5162+
mockDataAccess.Organization.findById.resolves(testOrganizations[0]);
5163+
mockDataAccess.Site.findById.resolves(testSites[0]);
5164+
mockTierClientStub.getAllEnrollment.resolves({
5165+
entitlement: { getTier: () => 'FREE_TRIAL' },
5166+
enrollments: [],
5167+
});
5168+
mockTierClientStub.getFirstEnrollment.resolves({
5169+
entitlement: { getTier: () => 'FREE_TRIAL' },
5170+
site: testSites[0],
5171+
});
5172+
5173+
await sitesController.resolveSite(context);
5174+
5175+
expect(mockTierClientStub.getFirstEnrollment).to.have.been.called;
5176+
});
5177+
});
50905178
});
50915179

50925180
describe('getBrandProfile', () => {

0 commit comments

Comments
 (0)